水煮MyBatis(十四)- 细说缓存刷新机制
前言
连续写几个大章节,乏了,这一章写点轻松的。什么是tcm,其实在上一章有提到,是CachingExecutor的一个类属性【TransactionalCacheManager 】,主要用来处理二级缓存的刷新逻辑。tcm里包含一个cache和transactionCache的Map集合,这里的【TransactionalCache】,也是Cache的一个实现,不过由于其用法特殊,没有和其他实现类放在一起讨论。
从代码可以看出,虽然tcm的名称和事务沾点边,但其实无论是否开启事务,只要激活二级缓存,都需要用到tcm。
// tcm是CachingExecutor的一个类属性
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
// 查询入口
public <E> List<E> query(...){
Cache cache = ms.getCache();
// 关键就是这一行,tcm.getObject(cache, key),从指定的cache中,查找指定的key
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 调用baseExecutor里的查询
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
为什么用tcm?
一起看看这一行代码
List<E> list = (List<E>) tcm.getObject(cache, key);
原本Cache的默认实现里,就包含一个map结构,用来存储缓存数据。缓存数据可以直接从cache中读取,比如:【cache.getObject(key)】。所以究竟是基于什么考虑,才用tcm的方式呢?
tcm读写
我们从TransactionalCacheManager的缓存读写方法来分析,源码如下:
//
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
// 读取缓存数据
public Object getObject(Cache cache, CacheKey key) {
// 从TransactionalCache中读取数据
return getTransactionalCache(cache).getObject(key);
}
// 数据写入TransactionalCache
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
// 从transactionalCaches中获取TransactionalCache对象,如果不存在,则新建一个映射关系,并返回新建的TransactionalCache对象
private TransactionalCache getTransactionalCache(Cache cache) {
return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
}
可以看到,数据写入了TransactionalCache,读取时,也是从TransactionalCache中返回的。需要注意的是新建TransactionalCache时,将前面一个参数传入了构造函数,从而与外部cache实例完成了绑定。
TransactionalCache做了什么?
TransactionalCache是cache的集合,新建时传入了玩不cache实例,在类中,需要重点关注的是这两个属性:
- entriesToAddOnCommit:等待刷入数据,在提交时刷入cache实例;
- entriesMissedInCache: 读取缓存时,没有数据的key集合,提交时,创建value为null的entry对象,放入cache实例;
这里只列出主要的几个方法
// 新建TransactionalCache时,传入的外部cache实例
private final Cache delegate;
// 提交的时候是否需要清除缓存,受Options里清除策略影响
private boolean clearOnCommit;
// 等待刷入数据
private final Map<Object, Object> entriesToAddOnCommit;
// 读取缓存时,没有数据的key集合
private final Set<Object> entriesMissedInCache;
@Override
public Object getObject(Object key) {
// 从内嵌的cache对象读取缓存数据
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
return object;
}
@Override
public void putObject(Object key, Object object) {
// 放入到临时集合,等待提交
entriesToAddOnCommit.put(key, object);
}
// cachingExecutor提交时,调用此方法
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
// 数据刷入cache实例
flushPendingEntries();
// 重置下面三个数据
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
private void flushPendingEntries() {
// 将entriesToAddOnCommit里所有的数据放入到cache实例
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
// 创建value为null的entry对象,放入cache实例
delegate.putObject(entry, null);
}
}
}
结构也比较简单,大概逻辑如下:
- 读取数据时,如果cache实例中不存在,则放入entriesMissedInCache集合;
- 保存数据时,放到临时集合【entriesToAddOnCommit】,等待提交再处理;
- cachingExecutor提交时,触发commit方法,把数据刷入到cache实例;
为什么这么做?
官方的说法如下
The 2nd level cache transactional buffer. This class holds all cache entries that are to be added to the 2nd level cache during a Session. Entries are sent to the cache when commit is called or discarded if the Session is rolled back. Blocking cache support has been added. Therefore any get() that returns a cache miss will be followed by a put() so any lock associated with the key can be released.
原因有三:
- 如果事务回滚,则将数据丢弃,不会放入到cache实例,这样可以有效避免脏数据;
- 事务期间,数据不会加载到cache实例,其他sqlSession不会读到临时数据;
- blockingCache中,release方法中,如果对象不存在,会抛出异常:
Detected an attempt at releasing unacquired lock. This should never happen.
,TransactionalCache会将查询过的所有key,无论是否存在缓存数据,都放入cache实例,一定程度上可以避免这个异常;
思考
tcm作为CachingExecutor的一个类对象,为什么还需要在内部维护一个cache的集合呢?毕竟一个executor的生命周期比较短暂,数据库操作结束就被关闭,而且二级缓存的实现类一般都会统一配置。原因可能有两个:
- 不同Mapper可以配置不同的eviction策略,那么就会有不同的cache实例;
- 一个事务之内,会同用sqlSession,CachingExecutor会处理多个数据库操作; 基于上面两个原因,tcm就需要维护一个cache集合了。
转载自:https://juejin.cn/post/7244820297278767161