MyBatis二级缓存
最后更新于:2022-08-13 12:43:40
一级缓存,是sqlSession级别的缓存,是默认开启的,
我们在使用MyBatis的时候其实就在使用,只是如果我们不去关注sql可能不会注意到它;
这篇文章我们就来了解一下另一个MyBatis内提供的缓存-二级缓存,
与一级缓存不同的二级缓存是mapper(namespace)级别的缓存,
也就是说多个sqlsession对同一个Mapper进行查询操作时是可以共享这个namespace中的缓存数据,
但是当我们去当前这个mapper中的进行增、删、改或者commit的时候,同样是会清空缓存的
从图中我们可以看出来,
sqlSession1在去查询数据的时候都会先去二级缓存中去查,
如果没有的话在去数据库中查询,同时会存入到二级缓存中,
等sqlsession2再去查询的时候,就可以直接从个缓存中拿了,
但是sqlsession3去查询之前sqlsession2去修改了查询的这个数据,
那么就会二级缓存就会被清空;这样才不会出现脏数据的情况。
好了接下来我们就去代码中去验证二级缓存存在,然后通过源码的方式来探究它的实现原理
MyBatis的二级缓存是需要手动开启的,这里需要两步配置
核心配置文件中(mybatis-config.xml)
映射文件中(UserMapper.xml)开启二级缓存,配置
测试代码
/**
* 使用二级缓存实体类要实现序列化接口
*
* @throws Exception
*/
@Test
public void testCache2() throws Exception {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = build.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
System.out.println("===============第一次查询===================");
User user = mapper.selectUserById(1);
sqlSession.close();
System.out.println(user);
// 第二次查询
System.out.println("===============第二次查询===================");
SqlSession sqlSession2 = build.openSession();
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = mapper2.selectUserById(1);
System.out.println(user2);
System.out.println("===============更新数据===================");
// user.setName("嘿嘿");
// user.setAge(21);
// mapper2.updateUserByUserId(user);
// sqlSession2.commit();
// sqlSession2.close();
// 第三次查询
System.out.println("===============第三次查询==================");
SqlSession sqlSession3 = build.openSession();
UserMapper mapper1 = sqlSession3.getMapper(UserMapper.class);
User user1 = mapper1.selectUserById(1);
System.out.println(user1);
sqlSession3.close();
}
第一次查询
看下测试结果
===============第一次查询===================
Cache Hit Ratio [test.UserMapper]: 0.0
==> Preparing: select * from t_user where id=?;
==> Parameters: 1(Integer)
<== Columns: id, name, age
<== Row: 1, 哈哈1, 12
<== Total: 1
User{id=1, username='null', age=12}
===============第二次查询===================
Cache Hit Ratio [test.UserMapper]: 0.5
User{id=1, username='null', age=12}
===============更新数据===================
===============第三次查询==================
Cache Hit Ratio [test.UserMapper]: 0.6666666666666666
User{id=1, username='null', age=12}
第一次查询的时候,出现了Cache Hit Ratio [test.UserMapper]: 0.0这个表示命中率的意思,0.0说明是缓存没有命中,并且发送了一条sql查询语句;第二查询的时候命中率是0.5说明两次查询命中了一次, 50%的命中率,更新操作被注释没有执行,第三次在此查询命中率是0,66666666说明是三次查询命中了2次,命中率约是66%,第二次和第三次查询都没有发送sql语句;说明二级缓存是存在并且生效的。
接着我们打开更新操作
===============第一次查询===================
==> Preparing: select * from t_user where id=?;
==> Parameters: 1(Integer)
<== Columns: id, name, age
<== Row: 1, 哈哈1, 12
<== Total: 1
User{id=1, username='null', age=12}
===============第二次查询===================
Cache Hit Ratio [test.UserMapper]: 0.5
User{id=1, username='null', age=12}
===============更新数据===================
==> Preparing: update t_user set name=?,age=? where id=?
==> Parameters: 嘿嘿(String), 21(Integer), 1(Integer)
<== Updates: 1
===============第三次查询==================
Cache Hit Ratio [test.UserMapper]: 0.3333333333333333
==> Preparing: select * from t_user where id=?;
==> Parameters: 1(Integer)
<== Columns: id, name, age
<== Row: 1, 嘿嘿, 21
<== Total: 1
User{id=1, username='嘿嘿', age=21}
从结果中我们可以看出,前两次查询和上次是一样的,命中率是0.5,执行更新操作之后,命中率变成了0.33,而且有发送了一条sql语句,说明三次查询中命中了1次,结果看清了,但是背后到底是怎么一回事儿,我们还的好好掰扯掰扯
源码探秘
接下来就是源码环节
在mybatis开始解析映射文件的时候,会解析我们配置的
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
// 批处理
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
// 重用执行器
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
// 简单执行器
executor = new SimpleExecutor(this, transaction);
}
// 判断是否开启全局配置缓存默认开启,
if (cacheEnabled) {
// CachingExecutor是对SimpleExecutor类的装饰器类,给其加上二级缓存的功能
executor = new CachingExecutor(executor);
}
// 拦截器链
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
第一次查询我们就不再说了,前面已经说了很多次了,咱么直接进入第二次查询,跟着断点走,在查询方法前会有创建缓存key的方法
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 创建缓存CacheKey对象,
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
List parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
// 返回创建的cacheKey
return cacheKey;
}
后需在查询的时候直接先去二级缓存查
public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 获取缓存对象
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 从缓存中取
List list = (List) tcm.getObject(cache, key);
if (list == null) {
// 如果二级缓存没有的话,回去进到这个查询方法中去一级缓存中找
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 同时放到tcm事务缓存对象中
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
这次就直接从存储到的存储到二级缓存中直接查找,找到之后返回
这里是从事务对象tcm中查找,而这里的事务缓存也是经过层层委托的对象
执行更新操作
跟着断点走,进到updata方法
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
// 必要的时候刷新缓存
flushCacheIfRequired(ms);
// 执行更新操作
return delegate.update(ms, parameterObject);
}
进到上面刷新的方法,会看到
public void clear() {
// 将这个commit时清空设置成了true
clearOnCommit = true;
entriesToAddOnCommit.clear();
}
继续跟着断点走的话,会发现在update()方法中只有清空一级缓存的缓存的方法,而真正清空缓存的方法是在commit方法中,所以二级缓存的使用一定注意提交事务,上面咱么说了他是委托了tcm对象管理管理缓存的
到这里基本二级缓存的内容就介绍完了,第三次查询和已经是清空缓存之后的查询,所以查询过程和第一次是一样的,这里就不再赘述
还有一点就是在解析完成
这里给大家提示一下的是MyBatis的缓存是和整个应用运行在同一个JVM中的,共享同一块堆内存,所以如果要缓存的数据量很大的话,建议是用其他的缓存框架,如:Reids、Memcache