引言

MyBatis是常见的Java数据库访问层框架。在日常工作中,开发人员多数情况下是使用MyBatis的默认缓存配置,但是MyBatis缓存机制有一些不足之处,在使用中容易引起脏数据,形成一些潜在的隐患。个人在业务开发中也处理过一些由于MyBatis缓存引发的开发问题,带着个人的兴趣,希望从应用及源码的角度为读者梳理MyBatis缓存机制。

基本原理

一级缓存

在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。

架构

MyBatis缓存机制 - 图1
image.png

配置

  1. <setting name="localCacheScope" value="SESSION"/>

说明:缓存级别有两个选项,SESSION或者STATEMENT,默认是SESSION级别,即在一个MyBatis会话中执行的所有语句,都会共享这一个缓存。一种是STATEMENT级别,可以理解为缓存只对当前执行的这一个Statement有效。

工作流程

MyBatis缓存机制 - 图3

失效场景

  • sqlsession变了,缓存失效。
  • sqlsession不变,查询条件不同,一级缓存失效。
  • sqlsession不变,中间发生了增删改操作,一级缓存失败。
  • sqlsession不变,手动清除缓存,一级缓存失败。

    示例

    1. public void getStudentById() throws Exception {
    2. SqlSession sqlSession = factory.openSession(true); // 自动提交事务
    3. StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
    4. System.out.println(studentMapper.getStudentById(1));
    5. System.out.println(studentMapper.getStudentById(1));
    6. }

    二级缓存

    在上文中提到的一级缓存中,其最大的共享范围就是一个SqlSession内部,如果多个SqlSession之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询。

    架构

    MyBatis缓存机制 - 图4
    image.png

    配置

    1. <setting name="cacheEnabled" value="true"/>
    2. <!-- 自定义配置 -->
    3. <cache type="org.apache.ibatis.cache.impl.PerpetualCache" size="1024" eviction="LRU" flushInterval="120000" readOnly="false"/>
    4. <!-- 应用 -->
    5. <cache-ref namespace="mapper.StudentMapper"/>
  • 参数说明

    • type:cache使用的类型,默认是PerpetualCache,这在一级缓存中提到过。
    • eviction: 定义回收的策略,常见的有FIFO,LRU。
    • flushInterval: 配置一定时间自动刷新缓存,单位是毫秒。
    • size: 最多缓存对象的个数。
    • readOnly: 是否只读,若配置可读写,则需要对应的实体类能够序列化。
    • blocking: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。

      清除策略

  • LRU :最近最少使用:移除最长时间不被使用的对象。

  • FIFO : – 先进先出:按对象进入缓存的顺序来移除它们。
  • SOFT :– 软引用:基于垃圾回收器状态和软引用规则移除对象。
  • WEAK :– 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

    示例

    1. @Test
    2. public void testCacheWithoutCommitOrClose() throws Exception {
    3. SqlSession sqlSession1 = factory.openSession(true);
    4. SqlSession sqlSession2 = factory.openSession(true);
    5. StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
    6. StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
    7. System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
    8. System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
    9. }

    整合第三方缓存

    1. 整合EhCache

  1. 导包。

    1. ehcache-core-2.6.8.jar、 mybatis-ehcache-1.0.3.jar
    2. slf4j-api-1.6.1.jar、 slf4j-log4j12-1.6.2.jar
  2. 配置ehcache.xml文件。

    1. <?xml version="1.0" encoding="UTF-8"?>
    2. <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    3. xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
    4. <!-- 磁盘保存路径 -->
    5. <diskStore path="G:\MyBatis\ehcache" />
    6. <defaultCache
    7. maxElementsInMemory="10000"
    8. maxElementsOnDisk="10000000"
    9. eternal="false"
    10. overflowToDisk="true"
    11. timeToIdleSeconds="120"
    12. timeToLiveSeconds="120"
    13. diskExpiryThreadIntervalSeconds="120"
    14. memoryStoreEvictionPolicy="LRU">
    15. </defaultCache>
    16. </ehcache>
  • 属性说明
    • diskStore:指定数据在磁盘中的存储位置。
    • defaultCache:当借助CacheManager.add(“demoCache”)创建Cache时,EhCache便会采用指定的的管理策略。

以下属性是必须的

  • maxElementsInMemory:在内存中缓存的element的最大数目。
  • maxElementsOnDisk:在磁盘上缓存的element的最大数目,若是0表示无穷大。
  • eternal:设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断。
  • overflowToDisk:设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上。

以下属性是可选的:

  • timeToIdleSeconds:当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时,这些数据便会删除,默认值是0,也就是可闲置时间无穷大。
  • timeToLiveSeconds:缓存element的有效生命期,默认是0.,也就是element存活时间无穷大。
  • diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小.默认是30MB.每个Cache都应该有自己的一个缓冲区。
  • diskPersistent:在VM重启的时候是否启用磁盘保存EhCache中的数据,默认是false。
  • diskExpiryThreadIntervalSeconds:磁盘缓存的清理线程运行间隔,默认是120秒。每个120s,相应的线程会进行一次EhCache中数据的清理工作。
  • memoryStoreEvictionPolicy:当内存缓存达到最大,有新的element加入的时候, 移除缓存中element的策略。默认是LRU(最近最少使用),可选的有LFU(最不常使用)和FIFO(先进先出)。
    1. 配置cache标签。

在mapper文件中配置mapper标签。

  1. <cache type="org.mybatis.caches.ehcache.EhcacheCache"></cache>

2. 整合Redis

  1. 添加依赖。

    1. <dependency>
    2. <groupId>org.mybatis.caches</groupId>
    3. <artifactId>mybatis-redis</artifactId>
    4. <version>1.0.0-beta2</version>
    5. </dependency>
  2. 在mapper文件的cache标签中加上type属性的值为Cache接口的实现类为RedisCache。

    1. <cache type="org.mybatis.caches.redis.RedisCache" />
    2. <select id="findAll" resultType="com.lagou.pojo.User" useCache="true">
    3. select * from user
    4. </select>
  3. 添加redis的连接信息,在resources目录下添加redis.properties文件。

    1. redis.host=localhost
    2. redis.port=6379
    3. redis.connectionTimeout=5000
    4. redis.password=
    5. redis.database=0

    最佳实践

  • 一级缓存

  1. MyBatis一级缓存的生命周期和SqlSession一致。
  2. MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺。
  3. MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement(如果一级缓存的范围是statement级别,则每次查询都清空一级缓存)。
  • 二级缓存

  1. MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
  2. MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
  3. 在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。

    参考

    美团技术团队:聊聊MyBatis缓存机制
    https://tech.meituan.com/2018/01/19/mybatis-cache.html