引言
MyBatis是常见的Java数据库访问层框架。在日常工作中,开发人员多数情况下是使用MyBatis的默认缓存配置,但是MyBatis缓存机制有一些不足之处,在使用中容易引起脏数据,形成一些潜在的隐患。个人在业务开发中也处理过一些由于MyBatis缓存引发的开发问题,带着个人的兴趣,希望从应用及源码的角度为读者梳理MyBatis缓存机制。
基本原理
一级缓存
在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。
架构
配置
<setting name="localCacheScope" value="SESSION"/>
说明:缓存级别有两个选项,SESSION或者STATEMENT,默认是SESSION级别,即在一个MyBatis会话中执行的所有语句,都会共享这一个缓存。一种是STATEMENT级别,可以理解为缓存只对当前执行的这一个Statement有效。
工作流程
失效场景
- sqlsession变了,缓存失效。
- sqlsession不变,查询条件不同,一级缓存失效。
- sqlsession不变,中间发生了增删改操作,一级缓存失败。
-
示例
public void getStudentById() throws Exception {
SqlSession sqlSession = factory.openSession(true); // 自动提交事务
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
System.out.println(studentMapper.getStudentById(1));
System.out.println(studentMapper.getStudentById(1));
}
二级缓存
在上文中提到的一级缓存中,其最大的共享范围就是一个SqlSession内部,如果多个SqlSession之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询。
架构
配置
<setting name="cacheEnabled" value="true"/>
<!-- 自定义配置 -->
<cache type="org.apache.ibatis.cache.impl.PerpetualCache" size="1024" eviction="LRU" flushInterval="120000" readOnly="false"/>
<!-- 应用 -->
<cache-ref namespace="mapper.StudentMapper"/>
参数说明
LRU
:最近最少使用:移除最长时间不被使用的对象。FIFO
: – 先进先出:按对象进入缓存的顺序来移除它们。SOFT
:– 软引用:基于垃圾回收器状态和软引用规则移除对象。WEAK
:– 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。示例
@Test
public void testCacheWithoutCommitOrClose() throws Exception {
SqlSession sqlSession1 = factory.openSession(true);
SqlSession sqlSession2 = factory.openSession(true);
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
}
整合第三方缓存
1. 整合EhCache
导包。
ehcache-core-2.6.8.jar、 mybatis-ehcache-1.0.3.jar
slf4j-api-1.6.1.jar、 slf4j-log4j12-1.6.2.jar
配置ehcache.xml文件。
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
<!-- 磁盘保存路径 -->
<diskStore path="G:\MyBatis\ehcache" />
<defaultCache
maxElementsInMemory="10000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</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(先进先出)。
- 配置cache标签。
在mapper文件中配置mapper标签。
<cache type="org.mybatis.caches.ehcache.EhcacheCache"></cache>
2. 整合Redis
添加依赖。
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
在mapper文件的cache标签中加上type属性的值为Cache接口的实现类为RedisCache。
<cache type="org.mybatis.caches.redis.RedisCache" />
<select id="findAll" resultType="com.lagou.pojo.User" useCache="true">
select * from user
</select>
添加redis的连接信息,在resources目录下添加redis.properties文件。
redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=0
最佳实践
- 一级缓存
- MyBatis一级缓存的生命周期和SqlSession一致。
- MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺。
- MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement(如果一级缓存的范围是statement级别,则每次查询都清空一级缓存)。
- 二级缓存
- MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
- MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
- 在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。
参考
美团技术团队:聊聊MyBatis缓存机制
https://tech.meituan.com/2018/01/19/mybatis-cache.html