Mybatis缓存
关于Mybatis的执行流程,点这里
一级缓存
一级缓存配置
在MyBatis的配置文件中,添加如下语句,就可以使用一级缓存。共有两个选项,SESSION或者STATEMENT,默认是SESSION级别,即在一个MyBatis会话中执行的所有语句,都会共享这一个缓存。一种是STATEMENT级别,可以理解为缓存只对当前执行的这一个Statement有效。
<setting name="localCacheScope" value="SESSION"/>
一级缓存执行流程
下图来自美团技术团队文章
验证一级缓存
数据库为MySQL8.0.19
数据表来自MySQL官方数据库employees
同一会话中的同一sql使用缓存
@Test
public void testFirstLevelCache() {
try (SqlSession sqlSession = sessionFactory.openSession()){
final DepartmentMapper mapper = sqlSession.getMapper(DepartmentMapper.class);
System.out.println(mapper.getById("d002") == mapper.getById("d002"));
}
}
执行结果,只发起一次数据库请求,两次查询结果为同一对象
执行修改操作后,一级缓存失效
try (SqlSession sqlSession = sessionFactory.openSession()) {
final DepartmentMapper mapper = sqlSession.getMapper(DepartmentMapper.class);
final Department department1 = mapper.getById("d002");
final Department department = new Department();
department.setDeptNo("d002");
department.setDeptName("Finance");
mapper.updateById(department);
final Department department2 = mapper.getById("d002");
System.out.println(department2 == department1);
}
执行结果,发起了两次数据库请求
基于sql语句的缓存
try (SqlSession sqlSession = sessionFactory.openSession()) {
final DepartmentMapper mapper = sqlSession.getMapper(DepartmentMapper.class);
mapper.getAll();
mapper.getById("d002");
}
一级缓存同一会话有效
try (SqlSession sqlSession1 = sessionFactory.openSession();
SqlSession sqlSession2 = sessionFactory.openSession()) {
final DepartmentMapper mapper1 = sqlSession1.getMapper(DepartmentMapper.class);
final DepartmentMapper mapper2 = sqlSession2.getMapper(DepartmentMapper.class);
System.out.println(mapper1.getById("d002"));
final Department department = new Department();
department.setDeptNo("d002");
department.setDeptName("我被修改了");
mapper2.updateById(department);
System.out.println(mapper1.getById("d002"));
System.out.println(mapper2.getById("d002"));
}
session2修改了数据,session1读取了自己缓存中的脏数据
手动清除缓存
try (SqlSession sqlSession = sessionFactory.openSession()){
final DepartmentMapper mapper = sqlSession.getMapper(DepartmentMapper.class);
mapper.getById("d002");
sqlSession.clearCache();
mapper.getById("d002");
}
Mybatis缓存的实现类
总结
- MyBatis一级缓存的生命周期和SqlSession一致。
- MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。
- 一级缓存失效四种情况
- 查询条件不同
- 执行了修改操作
- 不同的sqlSession
- 手动清除一级缓存(sqlSession的clearCache方法)
二级缓存
一级缓存中,其最大的共享范围就是一个SqlSession内部,如果多个SqlSession之间需要共享缓存,则需要使用到二级缓存。进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询
具体流程如下(下图来自美团技术团队文章)
二级缓存开启后,同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。
当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。
二级缓存配置
- 在主配置文件中
<setting name="cacheEnabled" value="true"/>
- 在需要开启二级缓存的mapper中
<cache/>
cache标签有以下选项
- type:cache使用的类型,默认是PerpetualCache,简单的Map实现。
- eviction: 定义回收的策略,常见的有FIFO,LRU。
- flushInterval: 配置一定时间自动刷新缓存,单位是毫秒。
- size: 最多缓存对象的个数。
- readOnly: 是否只读,若配置可读写,则需要对应的实体类能够序列化。
- blocking: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。
cache-ref代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。
<cache-ref namespace="com.rufeng.mapper.SalaryMapper"/>
二级缓存测试
提交事务之前二级缓存无效
SqlSession sqlSession1 = sessionFactory.openSession(false);
SqlSession sqlSession2 = sessionFactory.openSession(false);
final DepartmentMapper mapper1 = sqlSession1.getMapper(DepartmentMapper.class);
final DepartmentMapper mapper2 = sqlSession2.getMapper(DepartmentMapper.class);
mapper1.getById("d002");
// sqlSession1.close();
mapper2.getById("d002");
sqlSession1.close();
sqlSession2.close();
走两次数据库
二级缓存的默认实现是由TansactionalCacheManager管理的TansactionCache委托PerpetualCache实现,可以配置为上文提到的其他实现
TransactionalCacheManage主要方法
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
...
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}
private TransactionalCache getTransactionalCache(Cache cache) {
return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
}
TransactionalCahceManager的方法,其最终还是调用的TransactionalCache的方法,完全是管理中介的作用
TransactionalCache主要属性和方法
private final Cache delegate;
private boolean clearOnCommit;
private final Map<Object, Object> entriesToAddOnCommit;
private final Set<Object> entriesMissedInCache;
@Override
public Object getObject(Object key) {
// issue #116
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) {
return null;
} else {
return object;
}
}
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
public void rollback() {
unlockMissedEntries();
reset();
}
private void reset() {
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
- 调用put时,往entriesToaddOnCommit中添加
- 调用get时,从delegate中取,delegate是真正存储缓存数据的对象
- flushPendingEntries方法真正向缓存中添加数据
- commit,如果每次缓存只对一次事务有效(clearOnCommit),清空已有缓存,将暂存数据添加到缓存
提交事务后,二级缓存生效
SqlSession sqlSession1 = sessionFactory.openSession(false);
SqlSession sqlSession2 = sessionFactory.openSession(false);
final DepartmentMapper mapper1 = sqlSession1.getMapper(DepartmentMapper.class);
final DepartmentMapper mapper2 = sqlSession2.getMapper(DepartmentMapper.class);
mapper1.getById("d002");
sqlSession1.close();
mapper2.getById("d002");
sqlSession2.close();
提交之后,数据在delegate缓存中,getObject才能拿到缓存数据
修改数据后,缓存失效
SqlSession sqlSession1 = sessionFactory.openSession(false);
SqlSession sqlSession2 = sessionFactory.openSession(false);
final DepartmentMapper mapper1 = sqlSession1.getMapper(DepartmentMapper.class);
final DepartmentMapper mapper2 = sqlSession2.getMapper(DepartmentMapper.class);
mapper1.getById("d002");
final Department department = new Department();
department.setDeptNo("d002");
department.setDeptName("Finance");
mapper1.updateById(department);
sqlSession1.close();
mapper2.getById("d002");
sqlSession2.close();
不适用于多表情况
验证MyBatis的二级缓存不适应用于映射文件中存在多表查询的情况。
通常我们会为每个单表创建单独的映射文件,由于MyBatis的二级缓存是基于namespace的,多表查询语句所在的namspace无法感应到其他namespace中的语句对多表查询中涉及的表进行的修改,引发脏数据问题。
为了解决这个问题,可以使用Cache ref,让多个Mapper指向同一命名空间,这样两个映射文件对应的SQL操作都使用的是同一块缓存了。
不过这样做的后果是,缓存的粒度变粗了,多个Mapper namespace下的所有操作都会对缓存使用造成影响。
总结
总结
MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。
建议MyBatis缓存特性在生产环境中进行关闭,单纯作为一个ORM框架使用可能更为合适。
参考
Q.E.D.