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)。对照一下,就会很容易理解。

一级缓存

先通过通过下图我们来看看一级缓存的整个流转过程。

image

当用户第一次查询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的项目中,执行如下单元测试:

@Resource
private SqlSessionFactory sqlSessionFactory;

@Test
public void showDefaultCacheConfiguration() {
    System.out.println("一级缓存范围: " + sqlSessionFactory.getConfiguration().getLocalCacheScope());
    System.out.println("二级缓存是否被启用: " + sqlSessionFactory.getConfiguration().isCacheEnabled());
}

打印结果

一级缓存范围: SESSION
二级缓存是否被启用: true

也证明了上面的说法。

一级缓存验证

关于Spring Boot集成Mybatis我们在之前文章中已经专门讲过,这里不再赘述,直奔重点。先看一个单元测试:

@Resource
private SqlSessionFactory sqlSessionFactory;

@Test
void 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路径。

此时执行单元测试,会发现只有第一次查询了数据库,后面两次都未查询。

image

同时,在日志中只打印了一次查询数据库的SQL语句。

此时我们执行如下单元测试:

@Resource
private OrderMapper orderMapper;

@Test
void userFirstCache1() {
    for (int i = 0; i < 3; i++) {
        Order order = orderMapper.findById(1);
        log.info("订单信息:{}", order);
    }
}

会发现三次都查询了数据库,为什么呢?这是因为每次Mapper调用findById方法都会创建一个session,并且在执行完毕后关闭session。所以三次调用并不在一个session中,一级缓存并没有起作用。

而此时,如果将该方法放在一个事务当中,修改如下:

@Resource
private OrderMapper orderMapper;

@Transactional
@Test
void 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" >

下面看一下二级缓存的示意图。

image

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类实现如下:

@Data
public class Order implements Serializable {

    private int id;

    private String orderNo;

    private int amount;
}

由于二级缓存数据存储介质多种多样,不一定在内存有可能是硬盘或者远程服务器。所以,pojo类实现序列化接口是为了将缓存数据取出执行反序列化操作。

下面看一下具体的单元测试:

@Test
void userSecondCache() {
    for (int i = 0; i < 3; i++) {
        Order order = orderService.findById(1);
        log.info("订单信息:{}", order);
    }
}

由于开启了二级缓存,我们直接使用service进行查询,就可以发现缓存已经生效了。

image

在图中我们可以看到,还打印出了命中缓存的概率为:0.5。

禁用指定方法的二级缓存

由于cache是针对整个Mapper中的查询方法的,因此当某个方法不需要缓存时,可在对应的select标签中添加useCache值为false来禁用二级缓存。

<select id="findById" parameterType="int" resultMap="BaseResultMap" useCache="false">

小结

查询结果实时性要求不高的情况下可采用mybatis二级缓存降低数据库访问量,提高访问速度,同时配合设置缓存刷新间隔flushInterval来根据需要改变刷新缓存的频次。

通常情况下,如果同时设置了一级缓存和二级缓存,会先使用二级缓存的数据,然后再使用一级缓存的数据,最后才会访问数据库。

关于Mybatis缓存本篇文章就讲这么多,当大家心中对Mybatis的缓存有一个基础的印象之后,后面遇到类似的问题或bug时便有了思考的方向。

精品SpringBoot 2.x视频教程

《Spring Boot 2.x 视频教程全家桶》,精品Spring Boot 2.x视频教程,打造一套最全的Spring Boot 2.x视频教程。



SpringBoot集成Mybatis的一级缓存和二级缓存插图4

关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台

除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接

本文链接:https://choupangxia.com/2020/05/28/springboot-mybatis-cache/