MyBatis的缓存一级缓存机制
最后更新于:2022-08-13 12:24:51
MyBatis做为持久层的框架,跟数据库的交互式是最多的,在互联网的这种经常面临的高并发情况下,缓存的中要性不言而喻,比如大家常见的Redis、Memcached等,同样MyBatis也提供了缓存的的机制,MyBatis提供了两种缓存机制,一级缓存和二级缓存,接下来我们就来先来看下一级缓存的是怎么实现的
一级缓存
一级缓存是MyBatis中的默认提供的缓存的,也就是说,我们在使用Mybatis的时候本身就在使用,他是默认开启的, 一级缓存是SqlSession级别的缓存,只有在一个SqlSession内的查询才能共享缓存的数据,当我们关闭sqlSession的时候或者执行增删改查的操作的时候,缓存就会被清空
接下来我们来验证下,上代码
/**
* 测试一级缓存
*/
@Test
public void testCacheOne() throws Exception {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory builder = new SqlSessionFactoryBuilder().build(inputStream);
// 第一次获取sqlsession对象
SqlSession sqlSession = builder.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
System.out.println("第一次查询....");
User user = mapper.selectUserById(1);
System.out.println(user);
// 修改数据
// user.setName("哈哈");
// user.setAge(12);
// int i = mapper.updateUserByUserId(user);
// sqlSession.commit();
// 第二次查询
System.out.println("第一次查询....");
UserMapper mapper1 = sqlSession.getMapper(UserMapper.class);
User user1 = mapper1.selectUserById(1);
System.out.println(user1);
sqlSession.close();
}
验证一级缓存
看下运行结果图
第一次查询....
==> Preparing: select * from t_user where id=?;
==> Parameters: 1(Integer)
<== Columns: id, name, age
<== Row: 1, 嘿嘿, 21
<== Total: 1
User(id=1, name=嘿嘿, age=21)
第一次查询....
User(id=1, name=嘿嘿, age=21)
我们看到在第一次查询的时候,发送了sql去数据库中去查询,在第二次查询的时候没有发送任何sql,并且返回了同样的结果,这就说明在第二次查询的时候,并没有去数据库查询而是去缓存中拿了
清空一级缓存
接着我们在看下清空缓存的测试
第一次查询....
==> Preparing: select * from t_user where id=?;
==> Parameters: 1(Integer)
<== Columns: id, name, age
<== Row: 1, 嘿嘿, 21
<== Total: 1
User(id=1, name=嘿嘿, age=21)
==> Preparing: update t_user set name=?,age=? where id=?
==> Parameters: 哈哈(String), 12(Integer), 1(Integer)
<== Updates: 1
第二次查询....
==> Preparing: select * from t_user where id=?;
==> Parameters: 1(Integer)
<== Columns: id, name, age
<== Row: 1, 哈哈, 12
<== Total: 1
User(id=1, name=哈哈, age=12)
这里可以看出来,在我们执行了update更新语句的时候,MyBatis清空了一级缓存,第二次查询的时候,再一次发送了Sql去数据库中查询
知其然,知其所以然
到这里案例已经帮我证明的了一级缓存的存在,以及使用的过程中是如何清空的,但凡事都要问个究竟那接下来我们就一起探寻源码,看看一级缓存是咱么实现的,又是怎么清除的
咱么按照第二次的测试,先去查询,在执行update操作,然后再去查询的步骤,来探寻源码的执行
在这里我直接找到缓存的执行的逻辑,不在去看其他的执行细节,因为之前源码分析的环节,已经分析过详细的执行流程
执行查询
第一次
在执行openSession的时候,会创建Executor对象,根据执行器的类型ExecutorType选择的是SIMPLE类型,
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) {
// 创建缓存执行器
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
然后创建的SimpleExecutor对象,在这个对象创建的过程中,是调用的父类BaseExecutor对象,在父类实例化的过程中,创建PerpetualCache缓存对象
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
// 延迟加载
this.deferredLoads = new ConcurrentLinkedQueue<>();
// 创建本地缓存,即一级缓存
this.localCache = new PerpetualCache("LocalCache");
// 处理存储过程缓存,这里不讨论存储过程
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
紧接着又创建了CachingExecutor对象,进到这个对象中,这里是处理二级缓存的地方装饰器对象,在执行的查询的时候,MyBatis会先去二级缓存中去查找
Cache是从MappedStatement中获取到的,而MappedStatement又和每一个insert、delete、update、select绑定并在MyBatis启动的时候存入Configuration中:
public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
Cache cache = ms.getCache();
if(cache != null) {
this.flushCacheIfRequired(ms);
if(ms.isUseCache() && resultHandler == null) {
this.ensureNoOutParams(ms, parameterObject, boundSql);
List list = (List)this.tcm.getObject(cache, key);
if(list == null) {
list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
this.tcm.putObject(cache, key, list);
}
return list;
}
}
//先读取二级缓存,再去查询
return this.delegate.query(ms,
parameterObject,
rowBounds,
resultHandler,
key,
boundSql);
}
接下来的查询从一级缓存中查找,测试的是第一次查询,一级缓存中是没有数据
public List query(MappedStatement ms,
Object parameter,
RowBounds rowBounds,
ResultHandler resultHandler,
CacheKey key, BoundSql boundSql) throws SQLException {
// 删除不必要代码。。。。
List list;
try {
queryStack++;
// 去本地缓存中查询,这次是第一次查询,缓存中没有返回空
list = resultHandler == null ?
(List) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms,
key,
parameter,
boundSql);
} else {
// 数据库中查询
list = queryFromDatabase(ms, parameter, rowBounds,
resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
// 延迟加载
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() ==
LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
到这里一级缓存没有,就要去数据库中查询了
private List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List list;
// 设置缓存占位符
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 去数据库查询
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
// 把查询结果放入一级缓存
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
// 返回结果
return list;
}
更新数据
刚才的测试结果中,我看到更新数据会清空缓存这里我们主要看下在执行更新的时候,是怎么清空缓存的
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 清空一级缓存(本地缓存)
clearLocalCache();
// 执行更新操作
return doUpdate(ms, parameter);
}
在执行更新操作前 ,会先清空一级缓存,所以到这里我们就能才到,在第二次执行的时候,缓存中已经没有数据了,所以会在此执行和第一次一样的逻辑,继续从数据库中查找,所以在控制台我们看到会再一次的发送sql语句。其他的删除、新增等操作对对缓存的影响,也是同样的逻辑(因为新增和删除底层都是执行的update()方法),大家可以试着自己去看下
所以,这里第二查询其实和第一查询是一致的。这里就不在源码分析了。