关注公众号 不爱总结的麦穗 将不定期推送技术好文
什么是一级缓存
在程序运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,它们的结果极有可能完全相同。但是由于查询一次数据库的代价很大(I/O) ,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询。
接下来,我们通过源码来分析一级缓存的整个生命周期(ps:阅读之前,大家可以先去看看我其他关于MyBatis的文章,了解前置知识。)
缓存初始化
SqlSession只是一个MyBatis对外的接口,SqlSession将它的工作交给了Executor执行器这个角色来完成,负责完成对数据库的各种操作。
当创建一个SqlSession对象时,MyBatis会为这个SqlSession对象创建一个新的Executor执行器,而缓存信息就被维护在这个Executor执行器中。
openSessionFromDataSource方法会调用父类BaseExecutor的构造方法创建执行器。
- BaseExecutor#BaseExecutor
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<>();
// 创建PerpetualCache对象
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
一级缓存实际上就是使用PerpetualCache维护的,PerpetualCache实现原理其实很简单,其内部就是通过一个简单的HashMap<k,v> 来实现的。
- PerpetualCache类
public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap<>();
// 部分代码省略
}
对一级缓存的操作实则是对HashMap的操作。
缓存应用
还是基于之前的简单查询例子分析
- BaseExecutor#query
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler)
throws SQLException {
// 从MappedStatement对象中获取BoundSql对象
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 获取缓存Key
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
CacheKey=Statement Id + Offset + Limmit + Sql + Params。只要两条SQL的这五个值相同,即可以得到相同的CacheKey,也就是同一条Sql语句。
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
CacheKey key, BoundSql boundSql) throws SQLException {
// 部分代码省略
try {
queryStack++;
// 从缓存中获取结果
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 缓存中不存在,从数据库查询,并放入到缓存中
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
// 部分代码省略
return list;
}
缓存执行时序图
清空缓存
- BaseExecutor#commit/rollback
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
// 清空缓存
clearLocalCache();
flushStatements();
if (required) {
transaction.commit();
}
}
SqlSession 中执行 commit/rollback 操作(插入、更新、删除)会清空 SqlSession 中的一级缓存,保证缓存中始终保存的是最新的信息,避免脏读。
MyBatis一级缓存的生命周期和SqlSession一致,简单地使用了HashMap来维护。
什么是二级缓存
MyBatis的二级缓存是mapper级别(Application级别的缓存,多个SqlSession可以共用二级缓存,跨SqlSession的) 的缓存,它可以提高对数据库查询的效率。
缓存初始化
如果配置了"cacheEnabled=true(默认是true)",那么MyBatis在为SqlSession对象创建Executor对象时,会创建一个CachingExecutor象,这时SqlSession使用CachingExecutor对象来完成操作请求。
在MyBatis的映射XML中配置cache或者 cache-ref 。cache标签用于声明这个namespace使用二级缓存。
CachingExecutor持有了TransactionalCacheManager,即上述代码中的tcm。
TransactionalCacheManager中持有了一个Map,保存了Cache和用TransactionalCache包装后的Cache的映射关系。
Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>()
缓存应用
我们通过上述入口代码开始分析,从CachingExecutor的query方法展开。
- CachingExecutor#query
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler,
CacheKey key, BoundSql boundSql) throws SQLException {
Cache cache = ms.getCache();
// 如果配置文件中没有配置 <cache>,则 cache 为空
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 从二级缓存中获取数据
List<E> list = (List<E>) tcm.getObject(cache, key);
// 未命中缓存,从数据库获取
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 存入到 entriesToAddOnCommit 这个Map中,而非真实的缓存对象 delegate 中
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
存储二级缓存对象是放到了TransactionalCache.entriesToAddOnCommit这个map中,而非真实的缓存对象 delegate 中,因为直接存到 delegate 会导致脏数据问题。
直接存到 delegate 为什么会导致脏数据问题?
从图上可以看到,如果一个事务没提交就直接更新到缓存中,另外的事务就很有可能读到脏数据。
SqlSession提交或关闭之后二级缓存才会生效,这样就解决了上面的问题。
- TransactionalCache#flushPendingEntries
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
private void flushPendingEntries() {
// 将entriesToAddOnCommit的对象添加到delegate中,二级缓存才真正的生效
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);
}
}
}
缓存执行流程
总结
MyBatis 缓存是执行 SQL 优先从缓存中查询,查询不到再查数据库。
MyBatis 默认开启一级缓存,它有两个级别SESSION或者STATEMENT,默认是SESSION级别,一级缓存的作用域是 SqlSession。SqlSession执行insert/delete/update或者session.close()方法后,会清空一级缓存。
MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享(Mapper 级别的)。二级缓存开启后,同一个NameSpace下的所有操作语句,都影响着同一个Cache,是一个全局的变量。在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据。