【Mybatis】【缓存】Mybatis源码解析-缓存解析
1 前言
这节我们来看一下 Mybatis 的缓存,Cache 是缓存接口,定义了一些基本的缓存操作,所有缓存类都应该实 现该接口。 MyBatis 内部提供了丰富的缓存实现类,比如具有基本缓存功能的 PerpetualCache ,具有 LRU 策略的缓存 LruCache ,以及可保证线程安全的缓存 SynchronizedCache 和具备阻塞功能的缓存 BlockingCache 等。在以上几种缓存实现类中, PerpetualCache 相当于装饰模式中的 ConcreteComponent。LruCache、SynchronizedCache 和 BlockingCache 等相当于装饰模式中 的 ConcreteDecorator。它们的关系如下:
这节我们不会去深入的看每个缓存里边的具体实现,我们这节着重的去看下很多人常说的一二级缓存的具体作用时机和处理,以及缓存所带来的问题哈。
2 缓存类别
在我看了源码哈,我们的语句执行的过程中会涉及到三类缓存:
- 一级缓存:在执行器对象 Executor 里,而每个执行器又归属于 SqlSession,所以常说一级缓存的作用域是 SqlSession级别的。
- 二级缓存/事务缓存:在 CacheExecutor 里,在 MappedStatement 里,也就是我们 Mapper 里 cache 或者 cacheRef 标签,但统一都由 CacheExecutor 里的 TransactionalCacheManager进行管理,解决脏读下边会讲。
我们简单看下每种缓存的存储结构:
一级缓存固定就是 PerpetualCache 类型的,内部比较简单就是 Map<Object, Object> cache = new HashMap<>();
二级缓存的话是:Map<Cache,Map<Object, Object>> key 就是每个语句缓存,value 就是键值对。
3 CacheKey
在 MyBatis 中,引入缓存的目的是为提高查询效率,降低数据库压力。既然 MyBatis 引入了缓存,那么大家思考过缓存中的 key 和 value 的值分别是什么吗?大家可能很容易 能回答出 value 的内容,不就是 SQL 的查询结果吗。那 key 是什么呢?是字符串,还是 其他什么对象?如果是字符串的话,那么大家首先能想到的是用 SQL 语句作为 key。但这 是不对的,比如:
SELECT * FROM test where id > ?
查出来的结果可能是不同的,所以我们不能简单的使用 SQL 语句作 为 key。从这里可以看出来,运行时参数将会影响查询结果,因此我们的 key 应该涵盖运行 时参数。除此之外呢,如果进行分页查询,查询结果也会不同,因此 key 也应该涵盖分页参 数。综上,我们不能使用简单的 SQL 语句作为 key。应该考虑使用一种复合对象,能涵盖 可影响查询结果的因子。在 MyBatis 中,这种复合对象就是 CacheKey。下面来看一下它的定义。
public class CacheKey implements Cloneable, Serializable { private static final long serialVersionUID = 1146682552656046210L; // 乘子,默认为 37 private static final int DEFAULT_MULTIPLIER = 37; private static final int DEFAULT_HASHCODE = 17; private final int multiplier; // CacheKey 的 hashCode,综合了各种影响因子 private int hashcode; // 校验和 private long checksum; // 影响因子个数 private int count; // 影响因子集合 private List<Object> updateList;public CacheKey() { this.hashcode = DEFAULT_HASHCODE; this.multiplier = DEFAULT_MULTIPLIER; this.count = 0; this.updateList = new ArrayList<>(); } }
如上,除了 multiplier 是恒定不变的 ,其他变量将在更新操作中被修改。下面看一下 更新操作的代码。
/** 每当执行更新操作时,表示有新的影响因子参与计算 */ public void update(Object object) { int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); // 自增 count count++; // 计算校验和 checksum += baseHashCode; // 更新 baseHashCode baseHashCode *= count; // 计算 hashCode hashcode = multiplier * hashcode + baseHashCode; // 保存影响因子 updateList.add(object); }
当不断有新的影响因子参与计算时,hashcode 和 checksum 将会变得愈发复杂和随机。 这样可降低冲突率,使 CacheKey 可在缓存中更均匀的分布。CacheKey 最终要作为键存入 HashMap,因此它需要覆盖 equals 和 hashCode 方法。下面我们来看一下这两个方法的实现。
public boolean equals(Object object) { // 检测是否为同一个对象 if (this == object) { return true; } // 检测 object 是否为 CacheKey if (!(object instanceof CacheKey)) { return false; } final CacheKey cacheKey = (CacheKey) object; // 检测 hashCode 是否相等 if (hashcode != cacheKey.hashcode) { return false; } // 检测校验和是否相同 if (checksum != cacheKey.checksum) { return false; } // 检测 coutn 是否相同 if (count != cacheKey.count) { return false; } // 如果上面的检测都通过了,下面分别对每个影响因子进行比较 for (int i = 0; i < updateList.size(); i++) { Object thisObject = updateList.get(i); Object thatObject = cacheKey.updateList.get(i); if (!ArrayUtil.equals(thisObject, thatObject)) { return false; } } return true; } public int hashCode() { // 返回 hashcode 变量 return hashcode; }
对于 CacheKey 就不深入看了哈,你只要知道它就是通过一些参数变量等计算出你当前执行的 Sql 的一个 key,那么我们就进入缓存看看吧。
4 一级缓存
在进行数据库查询之前,MyBatis 首先会检查以及缓存中是否有相应的记录,若有的话 直接返回即可。一级缓存是数据库的最后一道防护,若一级缓存未命中,查询请求将落到数 据库上。一级缓存是在 BaseExecutor 被初始化的,我们上边讲过了,并且默认就是 PerpetualCache 类型的。 一级缓存所存储 从查询结果会在 MyBatis 执行更新操作(INSERT/UPDATE/DELETE),以及提交和回滚事 务时被清空。下面我们来看一下访问一级缓存的逻辑:
// BaseExecutor public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { // 我们的 Sql 语句 BoundSql boundSql = ms.getBoundSql(parameter); // 创建缓存 key CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); } // BaseExecutor public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); if (closed) { throw new ExecutorException("Executor was closed."); } if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); } List<E> list; 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--; } if (queryStack == 0) { for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } // issue #601 deferredLoads.clear(); if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } } return list; }
如上,在访问一级缓存之前,MyBatis 首先会调用 createCacheKey 方法创建 CacheKey。 下面我们来看一下 createCacheKey 方法的逻辑:
// BaseExecutor public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { if (closed) { throw new ExecutorException("Executor was closed."); } // 创建 CacheKey 对象 CacheKey cacheKey = new CacheKey(); // 将 MappedStatement 的 id 作为影响因子进行计算 cacheKey.update(ms.getId()); // RowBounds 用于分页查询,下面将它的两个字段作为影响因子进行计算 cacheKey.update(rowBounds.getOffset()); cacheKey.update(rowBounds.getLimit()); // 获取 sql 语句,并进行计算 cacheKey.update(boundSql.getSql()); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry(); // mimic DefaultParameterHandler logic for (ParameterMapping parameterMapping : parameterMappings) { if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; // 获取 SQL 中的占位符 #{xxx} 对应的运行时参数, 参与计算 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()); } return cacheKey; }
在计算 CacheKey 的过程中,有很多影响因子参与了计算。比如 MappedStatement 的 id 字段,SQL 语句,分页参数,运行时变量,Environment 的 id 字段等。通过让这些影响 因子参与计算,可以很好的区分不同查询请求。所以,我们可以简单的把 CacheKey 看做是 一个查询请求的 id。有了 CacheKey,我们就可以使用它读写缓存了。在上面代码中,若一 级缓存为命中,BaseExecutor 会调用 queryFromDatabase 查询数据库,并将查询结果写入缓 存中。下面看一下 queryFromDatabase 的逻辑。
// BaseExecutor private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> 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; }
那么一级缓存的清理我们说过是增删改的时候清理,首先我们要知道增删改其实都是调用的 update 来执行的,我们看下:
public int insert(String statement, Object parameter) { // 可以看到 insert 其实也是 update return update(statement, parameter); } public int delete(String statement, Object parameter) { // 删除也是调用更新 return update(statement, parameter); } // BaseExecutor 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); }
到此,关于一级缓存相关的逻辑就差不多分析完了。一级缓存的逻辑比较简单哈,总结的话就是一级缓存一直会存在并且在 SqlSession 的执行器中,查询的时候会根据你的语句以及参数多维度算出一个缓存 key,存入一级缓存,当执行增删改的时候,会清空当前执行器的缓存。
5 二级缓存
二级缓存构建在一级缓存之上,在收到查询请求时,MyBatis 首先会查询二级缓存。若 二级缓存未命中,再去查询一级缓存。与一级缓存不同,二级缓存和具体的命名空间绑定, 一级缓存则是和 SqlSession 绑定。在按照 MyBatis 规范使用 SqlSession 的情况下,一级缓 存不存在并发问题。二级缓存则不然,二级缓存可在多个命名空间间共享。这种情况下,会存在并发问题,因此需要针对性的去处理。除了并发问题,二级缓存还存在事务问题,相关 问题将在接下来进行分析。下面先来看一下访问二级缓存的逻辑。
// CachingExecutor public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { // 获取 BoundSql BoundSql boundSql = ms.getBoundSql(parameterObject); // 获取缓存 key CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); // 调用重载 return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { /** * 从 MappedStatement 中获取 Cache * 来源于你的 mapper 配置 有没有 cache cache-ref */ Cache cache = ms.getCache(); 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); // 存入二级缓存 tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } // 二级缓存中没有,就去一级缓存或者数据库中去查 return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
如上,注意二级缓存是从 MappedStatement 中获取的,而非由 CachingExecutor 创建。由 于 MappedStatement 存在于全局配置中,可以被多个 CachingExecutor 获取到,这样就会出现线程安全问题。除此之外,若不加以控制,多个事务共用一个缓存实例,会导致脏读问题。 线程安全问题可以通过 SynchronizedCache 装饰类解决,该装饰类会在 Cache 实例构造期间被添加上。至于脏读问题,需要借助其他类来处理,也就是上面代码中 tcm 变量对应的类型。 下面分析一下。
/** * 事务缓存管理器 */ public class TransactionalCacheManager { // Cache 与 TransactionalCache 的映射关系表 private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>(); /** * 获取 TransactionalCache, TransactionalCache里又有我们具体的二级缓存 cache,也就是包装了 增强事务功能,防止脏读 * 下边也都是调用 TransactionalCache 进行的 * @param cache 二级缓存 */ private TransactionalCache getTransactionalCache(Cache cache) { return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new); } public void clear(Cache cache) { getTransactionalCache(cache).clear(); } 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(); } } }
TransactionalCacheManager 内部维护了 Cache 实例与 TransactionalCache 实例间的映 射关系,该类也仅负责维护两者的映射关系,真正做事的还是 TransactionalCache。 TransactionalCache 是一种缓存装饰器,可以为 Cache 实例增加事务功能。我在之前提到的 脏读问题正是由该类进行处理的。下面分析一下该类的逻辑。
public class TransactionalCache implements Cache { private static final Log log = LogFactory.getLog(TransactionalCache.class); private final Cache delegate; private boolean clearOnCommit; // 在事务被提交前,所有从数据库中查询的结果将缓存在此集合中 private final Map<Object, Object> entriesToAddOnCommit; // 在事务被提交前,当缓存未命中时,CacheKey 将会被存储在此集合中 private final Set<Object> entriesMissedInCache; public TransactionalCache(Cache delegate) { this.delegate = delegate; this.clearOnCommit = false; this.entriesToAddOnCommit = new HashMap<>(); this.entriesMissedInCache = new HashSet<>(); } @Override public String getId() { return delegate.getId(); } @Override public int getSize() { return delegate.getSize(); } @Override public Object getObject(Object key) { // 查询 delegate 所代表的缓存 Object object = delegate.getObject(key); if (object == null) { // 缓存未命中,则将 key 存入到 entriesMissedInCache 中 entriesMissedInCache.add(key); } // issue #146 if (clearOnCommit) { return null; } else { return object; } } @Override public void putObject(Object key, Object object) { // 将键值对存入到 entriesToAddOnCommit 中,而非 delegate 缓存中 entriesToAddOnCommit.put(key, object); } @Override public Object removeObject(Object key) { return null; } @Override public void clear() { clearOnCommit = true; // 清空 entriesToAddOnCommit,但不清空 delegate 缓存 entriesToAddOnCommit.clear(); } public void commit() { // 根据 clearOnCommit 的值决定是否清空 delegate if (clearOnCommit) { delegate.clear(); } // 刷新未缓存的结果到 delegate 缓存中 flushPendingEntries(); // 重置 entriesToAddOnCommit 和 entriesMissedInCache 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()) { // 将 entriesToAddOnCommit 中的内容转存到 delegate 中 delegate.putObject(entry.getKey(), entry.getValue()); } for (Object entry : entriesMissedInCache) { if (!entriesToAddOnCommit.containsKey(entry)) { // 存入空值 delegate.putObject(entry, null); } } } private void unlockMissedEntries() { for (Object entry : entriesMissedInCache) { try { // 调用 removeObject 移除某个缓存 delegate.removeObject(entry); } catch (Exception e) { log.warn("Unexpected exception while notifiying a rollback to the cache adapter. " + "Consider upgrading your cache adapter to the latest version. Cause: " + e); } } } }
可以看到 TransactionalCache 就是给我们的二级缓存外边包住,在你还未提价时,你的东西都被放在了待提交的集合中,当你提交后,才会把内容放到二级缓存中,解决了脏读,但是不可重复读幻读等问题还是存在的,其实在我们的实际开发中,倒是还没用过二级缓存哈,不知道项目上用不用。
二级缓存什么时候清理呢,也是进行增删改的时候,会对二级缓存进行清理,我们看下:
// CachingExecutor public int update(MappedStatement ms, Object parameterObject) throws SQLException { /** * 创建 MappedStatement 对象的时候的属性 flushCacheRequired 查询false 增删改都是true * MapperBuilderAssistant 中 flushCacheRequired(valueOrDefault(flushCache, !isSelect)) * 这里清的是事务缓存 TransactionalCacheManager负责管理,里边维护着 Map<Cache, TransactionalCache> * Map<Cache, TransactionalCache> key->cache 就是 mapper 里的<cache/>标签 value->TransactionalCache * TransactionalCache:Map<Object, Object> entriesToAddOnCommit; * 事务缓存,解决用来解决脏读的(后续单独讲哈) */ flushCacheIfRequired(ms); // BaseExecutor 执行器去执行 return delegate.update(ms, parameterObject); } private void flushCacheIfRequired(MappedStatement ms) { Cache cache = ms.getCache(); if (cache != null && ms.isFlushCacheRequired()) { // 事务缓存清理 tcm.clear(cache); } }
6 小结
这节我们看了 Mybatis 的一二级缓存,一级缓存相对简单作用域在 SqlSession的执行器,二级缓存在 MapperedStatement 里,经过 CacheExecutor 里会用TranscationCache进行包装,解决脏读的问题,但是不可重复读还是会存在,我就在想一级缓存它是 SqlSession里的,那么我两次的SqlSession不一样是不是也会脏读呢?我感觉也是会的,好啦,这节就暂时说这么多,有理解不对的地方欢迎指正哈。