一、概念说明
myabtis的缓存分为一级缓存和二级缓存,默认开启一级缓存,关闭二级缓存,
一级缓存时sqlSession级别,二级缓存是namespace级别。
1、一级缓存
mybatis的一级缓存默认是开启的,它在一个sqlSession会话里面的所有查询操作都会保存到缓存中,一般来说一个请求中的所有增删改查操作都是在同一个sqlSession里面的,所以我们可以认为每个请求都有自己的一级缓存,如果同一个sqlSession会话中2个查询中间有一个 insert 、update或delete 语句,那么之前查询的所有缓存都会清空。
使用MyBatis开启一次和数据库的会话,MyBatis会创建出一个SqlSession对象表示一次数据库会话,在对数据库的一次会话中, 有可能会反复地执行完全相同的查询语句,每一次查询都会去查一次数据库,为了减少资源浪费,mybaits提供了一种缓存的方式(一级缓存)。
一级缓存失效:
如果同一个sqlSession会话中2 个查询中间有一个 insert 、update或delete 语句,那么之前查询的所有缓存都会清空。因为每次增删改操作都有可能会改变原来的数据,所以必须刷新缓存;
2、二级缓存
二级缓存是全局的,也就是说;多个请求可以共用一个缓存,二级缓存需要手动开启。
二级缓存针对的是同一个namespace,所以建议是在单表操作的Mapper中使用,或者是在相关表的Mapper文件中共享同一个缓存。
一级缓存无过期时间,只有生命周期,缓存会先放在一级缓存中,当sqlSession会话提交或者关闭时才会将一级缓存刷新到二级缓存中;开启二级缓存后,用户查询时,会先去二级缓存中找,找不到在去一级缓存中找,然后才去数据库查询;
二级缓存失效:
所有的update操作(insert,delete,uptede)都会触发缓存的刷新,从而导致二级缓存失效,所以二级缓存适合在读多写少的场景中开启。
二级缓存过期时间:
需要注意的是,并不是key-value的过期时间,而是这个cache的过期时间,是flushInterval,意味着整个清空缓存cache,所以不需要后台线程去定时检测。
每当存取数据的时候,都有检测一下cache的生命时间,默认是1小时,如果这个cache存活了一个小时,那么将整个清空一下。
3、比较
开启方式
一级缓存是默认开启的,二级缓存默认关闭。
作用范围
一级缓存是会话级别的缓存,即sqlSession级别,会话结束,清除会话中的缓存数据,实际代码中通过通过开启事务让多个数据库操作共享一个sqlSession。
二级缓存: 全局级别,也叫namespace级别,会话结束,缓存依然存在,多个请求可以共享缓存数据。
缓存位置
一级缓存由于是sqlSession级别,本质上是在JVM中创建一个Map集合对象保存缓存数据,所以缓存数据保留的地方是本地JVM内存中。
二级缓存默认也是保存在JVM中,但是可以通过配置将缓存数据保存到第三方缓存中,比如ehcache、redis。保存在redis这些的分布式缓存中,能提供更好的分布式场景的支持。
缓存过期
一级缓存无过期时间,只有生命周期,缓存会先放在一级缓存中,当sqlSession会话提交或者关闭时才会将一级缓存刷新到二级缓存中;开启二级缓存后,用户查询时,会先去二级缓存中找,找不到在去一级缓存中找,然后才去数据库查询;
二级缓存的过期时间默认是1小时,如果这个cache存活了一个小时,那么将整个清空一下。需要注意的是,并不是key-value的过期时间,而是这个cache的过期时间,是flushInterval,意味着整个清空缓存cache,所以不需要后台线程去定时检测,每当存取数据的时候,都有检测一下cache的生命时间。
小结:
一级缓存的作用在我看来在实际业务场景中作用真的非常有限,因为需要在一个事务方法中重复查询的需求场景真的太少,而且由于Mysql数据库的MVCC机制以及事务隔离机制-可重复读的能力,会导致同一个事务方法内多次执行相同的查询必定会得到相同的结果,所以在事务范围内的重复查询基本没什么实际作用。
设计一级缓存设计的意义,可能更多的是为二级缓存的实现做铺垫。
所以,如果关闭了mybatis的一级缓存,二级缓存将不会生效。
二、mybatis缓存的生命周期
mybatis的查询缓存会先放在一级缓存中,当sqlSession会话提交或者关闭时才会将一级缓存刷新到二级缓存中;
开启二级缓存后,用户查询时,会先去二级缓存中找,找不到在去一级缓存中找,然后才去数据库查询;
三、一级缓存的使用
spring开启事务管理的方法在方法执行过程中只会创建一个sqlsession。
这是因为事务管理下的sql执行方式是BATCH,只会与数据库交互一次,一次执行完所有的sql,所以只会创建一个sqlsession;
不开启事务:
@Override public GoodsStock getStock(Integer goodsId) { log.info("第1次查询"); GoodsStock goodsStock = goodsStockMapper.getStock(goodsId); log.info("第2次查询"); goodsStock = goodsStockMapper.getStock(goodsId); return goodsStock; }
执行结果:
开启事务:
@Override @Transactional(rollbackFor = Exception.class) public GoodsStock getStock(Integer goodsId) { log.info("第1次查询"); GoodsStock goodsStock = goodsStockMapper.getStock(goodsId); log.info("第2次查询"); goodsStock = goodsStockMapper.getStock(goodsId); return goodsStock; }
执行结果:
可以看到,第2次查询并没有打印查询SQL,说明命中了缓存。
关闭一级缓存:
在配置文件中添加如下属性
#一级缓存关闭,默认值SESSION,表示开启一级缓存,statement表示关闭一级缓存
mybatis.configuration.local-cache-scope=statement
再次请求:
通过添加配置关闭mybatis的一级缓存后,第2次查询打印了SQL,进行了查库操作。
说实话,一级缓存比较鸡肋,一般在同一个方法内,很少会进行重复的查询,在实际业务中的真实使用场景暂时没有想到。目前的实际意义更多的是为mybatis的二级缓存做一个铺垫。
关于一级缓存的使用注意:
1、一般在一个方法内需要用相同条件查询多次的场景其实非常少见,因为方法体内对象都是可见共享的,没必要再次进行查询。
2、如果需要再次查询判断之前的查询结果是否已经出现变更,这时除了需要关闭一级缓存外,还需要注意事务的隔离级别。
比如,
如果事务隔离级别为可重复读时,在本事务中尽管执行多次查询,由于会对查询的数据在事务范围内加读锁,所以查询的数据一定是相同的,没有重复查下的必要。
如果事务隔离级别为读已提交,需要进行重复查询判断数据的变化情况,那么需要关闭mybatis的一级缓存。
不过,推荐在需要多次查询做数据比对的场景,最好还是关闭事务,这样就可以保障每次查询都进行查库操作,获取最新的数据。
事务为可重复读级别REPEATABLE_READ,没有再次查询的必要,数据一定不会发生变更:
@Override @Transactional(rollbackFor = Exception.class,isolation=Isolation.REPEATABLE_READ,readOnly = true) public GoodsStock getStock(Integer goodsId) { log.info("第1次查询"); //由于是REPEATABLE_READ可重复读隔离级别,在事务范围内会对读取到的数据加锁,所以读取到的数据一定不会发生变更,没有再次查询的必要 GoodsStock goodsStock = goodsStockMapper.getStock(goodsId); log.info("第2次查询"); goodsStock = goodsStockMapper.getStock(goodsId); return goodsStock; }
事务为读已提交READ_COMMITTED,由于读锁会立即释放,导致事务范围内已经查询到的数据可能被其他事务修改,如果需要重复查库判断数据变化,那么必须关闭mybatis的一级缓存,不然2次查询不会进行查库操作。
如果在1次查询和2 次查询中的逻辑处理有一个 insert 、update或delete 操作,那么之前查询的所有缓存都会清空。
2次查询也会进行查库操作。
@Override @Transactional(rollbackFor = Exception.class,isolation=Isolation.READ_COMMITTED,readOnly = true) public GoodsStock getStock(Integer goodsId) { log.info("第1次查询"); //由于是REPEATABLE_READ可重复读隔离级别,在事务范围内会对读取到的数据加锁,所以读取到的数据一定不会发生变更,没有再次查询的必要 GoodsStock goodsStock1 = goodsStockMapper.getStock(goodsId); log.info("逻辑处理"); log.info("第2次查询,判断变化,需要关闭mybatis的一级缓存"); GoodsStock goodsStock2 = goodsStockMapper.getStock(goodsId); return goodsStock2; }
四、二级缓存的使用
二级缓存相关的配置有三个地方:
1、mybatis-config中有一个全局配置属性,这个不配置也行,因为默认就是true。
<setting name="cacheEnabled" value="true"/>
spring boot中也可以通过配置项开启二级缓存:
#二级缓存开启 mybatis.configuration.cache-enabled=true
2、在Mapper映射文件内需要配置缓存标签:
<cache eviction="LRU" flushInterval="60000" size="1000"/>
配置项说明:
eviction 淘汰策略,LRU、FIFO
size 缓存个数
flushInterval 刷新频率,即缓存过期时间
LruCache LRU淘汰策略缓存(默认淘汰策略) 当缓存达到上限,删除最近最少使用缓存 eviction=“LRU”
FifoCache FIFO淘汰策略缓存 当缓存达到上限,删除最先入队的缓存 eviction=“FIFO”
3、在select查询语句标签上配置useCache属性,如下:
<select id="selectUserAndJob" resultMap="JobResultMap2" useCache="true"> select * from lw_user </select>
以上配置第1点是默认开启的,也就是说我们只要配置第2点就可以打开二级缓存了,而第3点是当我们需要针对某一条语句来配置二级缓存的时候则可以使用。
4、开启二级缓存后,映射的pojo对象需要实现序列化接口
执行结果报序列化异常,那是因为我们映射的pojo对象未实现序列化接口,说明我们从缓存数据中读取数据需要进行反序列化,这是因 为mybatis的二级缓存的缓存介质有多种多样,而并不一定是在内存中,所以需要我们对pojo对象进行序列化,只要实现序列化接口即可。
五、自定义二级缓存
默认的二级缓存也是存储在本地缓存,对于微服务下是可能出现脏读的情况的,这时可能会需要自定义缓存,比如利用redis来存储缓存,而不是存储在本地内存当中。
MyBatis官方也提供了第三方缓存的支持引入pom文件:
<dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-redis</artifactId> <version>1.0.0-beta2</version> </dependency>
然后缓存配置如下:
<cache type="org.mybatis.caches.redis.RedisCache"></cache>
然后在默认的resource路径下新建一个redis.properties文件:
host=localhost port=6379
六、mybatis缓存、spring缓存和redis缓存的使用比较
mybatis缓存——dao层的缓存,主要是对select查库操作的结果进行缓存。
spring缓存——方法级别的缓存,主要通过在方法上添加缓存注解@CachePut、@CacheEvict来实现。
redisTemplate缓存——代码级别的缓存,主要是在方法体内部通过redisTemplate模板类编码控制缓存读写。
mybatis缓存只能对sql查询结果进行缓存,spring 缓存只能对方法执行结果进行缓存,不能对方法执行过程中的中间对象进行缓存。这两者的优点都是使用简单,只需简单的配置或添加注解即可实现缓存,基本不需要编码。
缺点是缓存的控制范围都是方法级别,不能缓存方法执行过程中的中间对象,有一定的使用局限性。
而通过redisTemplate进行编码控制缓存,优点是控制灵活,缺点是需要编码实现。
实际项目中大家可以根据需要选择怎么使用缓存,但是要注意,针对单一方法,缓存不要混用,容易造成逻辑混乱,定位问题困难等问题。
总结
本文主要是针对mybatis的一级缓存和二级缓存的区别和使用进行了详细介绍。重点掌握一下方面
1、一级缓存和二级缓存的区别
2、mybatis缓存的生命周期
3、如何开启二级缓存,二级缓存什么时候失效,如何将本地的二级缓存升级到采用redis支持分布式的二级缓存?
4、二级缓存的淘汰策略有哪些
5、mybatis缓存、spring缓存和redis缓存的比较。
mybatis的一级缓存是为了二级缓存做铺垫,如果采用二级缓存,推荐升级到采用redis存储缓存数据,这样能更好的支持分布式。