SpringBoot集成Mybatis几乎已经成为大多数项目的标配了,但在使用的过程中Mybatis的缓存功能往往会被大家遗忘,甚至很多开发者都没意识到在SpringBoot集成Mybatis还有一级缓存和二级缓存的事。
本来没计划写本篇文章,但在实践的过程掉坑里了,当从坑中爬起来时,发现有必要给大家写写Mybatis的缓存。
遇到什么样的坑
事情是这样的:项目中使用了乐观锁,并进行了失败尝试(3次)。但运行的时候发现尝试也是失败的。起初以为是并发问题,然后把尝试次数无限放大,发现次次都是失败的。
这其中一定有问题,经过研究发现是Mybatis的一级缓存导致的,于是专门研究了Mybatis的一级和二级缓存分享给大家。
缓存存在的意义
其实在日常的项目中,我们几乎都会用到缓存,比如一些不怎么改变的配置项,会采用缓存来减少数据库的压力。Mybatis的一级二级缓存所起到的作用也是相同的。都是为了减少数据库压力,提高系统性能。
两个基本缓存的区别
Mybatis的一级缓存与二级缓存的主要区别是它们所缓存的范围不同。一级缓存是单个session级别的,二级缓存是多个session级别的,只不过多个session需要是同一个namespace下的。关于细节我们后面会逐一介绍。
这里所说的session与我们在Http请求中所说的session可以类别,但并不是同一个session。Http中是session指定的是HttpSession,而这里所说的session是指的查询数据库的SqlSession。
一次网页请求,可以创建一个session(HttpSession),一次数据库查询操作同样会创建一个session(SqlSession)。对照一下,就会很容易理解。
一级缓存
先通过通过下图我们来看看一级缓存的整个流转过程。当用户第一次查询id为1的订单时,缓存中没有数据,所以从数据库中进行加载,加载完成会进行缓存。这里缓存的位置就是内存中的一块空间,数据格式为HashMap。
当第二次读取时,便会直接读取缓存中的数据。当SqlSession执行commit操作(包括插入、更新、删除)时,会清空SqlSession的一级缓存,主要目的是确保缓存中的数据是最新的,避免脏读。
一级缓存是本地(局部)缓存,不能被关闭,只能配置缓存范围:SESSION或STATEMENT。也就是说一级缓存不需要在配置文件去配置,默认开启。
Spring Boot中Mybatis缓存的默认配置
看一下Mybatis源码中的org.apache.ibatis.session.Configuration类的部分源码:
public Configuration() { // ... this.cacheEnabled = true; this.localCacheScope = LocalCacheScope.SESSION; // ... }
我们可以看到缓存是默认开启的,而localCacheScope默认为Session级别。LocalCacheScope中只定义了SESSION和STATEMENT两个枚举项。
需要注意的是cacheEnabled配置的是二级缓存,而localCacheScope配置的是一级缓存。默认情况下SpringBoot集成Mybatis时一级缓存和二级缓存都是开启状态。
在Spring Boot集成Mybatis的项目中,执行如下单元测试:
@Resourceprivate SqlSessionFactory sqlSessionFactory; @Testpublic void showDefaultCacheConfiguration() { System.out.println("一级缓存范围: " + sqlSessionFactory.getConfiguration().getLocalCacheScope()); System.out.println("二级缓存是否被启用: " + sqlSessionFactory.getConfiguration().isCacheEnabled());}
打印结果
一级缓存范围: SESSION 二级缓存是否被启用: true
也证明了上面的说法。
一级缓存验证
关于Spring Boot集成Mybatis我们在之前文章中已经专门讲过,这里不再赘述,直奔重点。先看一个单元测试:
@Resourceprivate SqlSessionFactory sqlSessionFactory; @Testvoid userFirstCache() { SqlSession sqlSession = sqlSessionFactory.openSession(); OrderMapper orderMapper = sqlSession.getMapper(OrderMapper.class); for (int i = 0; i < 3; i++) { Order order = orderMapper.findById(1); log.info("订单信息:{}", order); }}
在该单元测试中,手动获取SqlSession,并通过SqlSession获得OrderMapper,然后进行数据的查询。执行单元测试之前需在application.properties中配置打印SQL语句的日志:
logging.level.com.secbro.mapper= debug
注意:level后面的包名需要替换成mapper所在的package路径。
此时执行单元测试,会发现只有第一次查询了数据库,后面两次都未查询。同时,在日志中只打印了一次查询数据库的SQL语句。
此时我们执行如下单元测试:
@Resourceprivate OrderMapper orderMapper; @Testvoid userFirstCache1() { for (int i = 0; i < 3; i++) { Order order = orderMapper.findById(1); log.info("订单信息:{}", order); }}
会发现三次都查询了数据库,为什么呢?这是因为每次Mapper调用findById方法都会创建一个session,并且在执行完毕后关闭session。所以三次调用并不在一个session中,一级缓存并没有起作用。
而此时,如果将该方法放在一个事务当中,修改如下:
@Resourceprivate OrderMapper orderMapper; @Transactional@Testvoid userFirstCache1() { for (int i = 0; i < 3; i++) { Order order = orderMapper.findById(1); log.info("订单信息:{}", order); }}
此时,我们发现一级缓存又生效了。而前文提到的乐观锁重试的Bug就是由于在此场景下使用了一级缓存,查询不到最新的数据库数据导致的。此处也是大家在使用的过程中需要留意的。
实践中,将Mybatis和Spring进行整合开发,事务控制在service中。如果是执行两次service调用查询相同的用户信息,不走一级缓存,因为Service方法结束,SqlSession就关闭,一级缓存就清空。
二级缓存
二级缓存是针对不同SqlSession直接的缓存,可以理解为mapper级别。这些SqlSession需要是同一个namespace。那namespace在哪里体现呢?
就是我们在xxMapper.xml文件中配置的namespace:
<mapper namespace="com.secbro.mapper.OrderMapper" >
下面看一下二级缓存的示意图。sqlSession1去查询用户id为1的订单信息,查询到用户信息会将查询数据存储到二级缓存中。sqlSession2去查询时便会直接通过二级缓存进行查询。
二级缓存与一级缓存区别,二级缓存的范围更大,多个sqlSession可以共享一个OrderMapper的二级缓存区域。数据类型仍然为HashMap。每一个namespace的mapper都有一个二缓存区域,两个mapper的namespace如果相同,这两个mapper执行sql查询到数据将存在相同的二级缓存区域中。
二级缓存的开启
在上面的Configuration类中我们已经看到默认开启了二级缓存,此开启操作可以通过在application中进行开启或关闭(false):
mybatis.configuration.cache-enabled=true
当然,也可以在SqlMapConfig.xml中加入:
<setting name="cacheEnabled"value="true"/>
来开启。
此时只是完成了二级缓存的全局开关,但并没有针对具体的Mapper生效。如果需要对指定的Mapper使用二级缓存,还需要在对应的xml文件中配置如下内容:
<mapper namespace="com.secbro.mapper.OrderMapper" > <cache/> <!--省略其他内容--> </mapper>
此时,该namespace下的Mapper便开启了二级缓存。
二级缓存实例
二级缓存需要查询结果映射的pojo对象实现java.io.Serializable接口。如果存在父类、成员pojo都需要实现序列化接口。否则,执行的过程中会直接报错。
此时,Order类实现如下:
@Datapublic class Order implements Serializable { private int id; private String orderNo; private int amount;}
由于二级缓存数据存储介质多种多样,不一定在内存有可能是硬盘或者远程服务器。所以,pojo类实现序列化接口是为了将缓存数据取出执行反序列化操作。
下面看一下具体的单元测试:
@Testvoid userSecondCache() { for (int i = 0; i < 3; i++) { Order order = orderService.findById(1); log.info("订单信息:{}", order); }}
由于开启了二级缓存,我们直接使用service进行查询,就可以发现缓存已经生效了。在图中我们可以看到,还打印出了命中缓存的概率为:0.5。
禁用指定方法的二级缓存
由于cache是针对整个Mapper中的查询方法的,因此当某个方法不需要缓存时,可在对应的select标签中添加useCache值为false来禁用二级缓存。
<select id="findById" parameterType="int" resultMap="BaseResultMap" useCache="false">
小结
查询结果实时性要求不高的情况下可采用mybatis二级缓存降低数据库访问量,提高访问速度,同时配合设置缓存刷新间隔flushInterval来根据需要改变刷新缓存的频次。
通常情况下,如果同时设置了一级缓存和二级缓存,会先使用二级缓存的数据,然后再使用一级缓存的数据,最后才会访问数据库。
关于Mybatis缓存本篇文章就讲这么多,当大家心中对Mybatis的缓存有一个基础的印象之后,后面遇到类似的问题或bug时便有了思考的方向。