二级缓存构建在一级缓存之上,在收到查询请求时,MyBatis首先会查询二级缓存。若二级缓存未命中,再去查询一级缓存。
- 二级缓存和具体的命名空间绑定
- 一级缓存和SqlSession绑定
在按照 MyBatis 规范使用SqlSession的情况下,一级缓存不存在并发问题(因为采用了LOCK)。二级缓存则不然,二级缓存可在多个命名空间共享。这种情况下,会存在并发问题,因此需要针对性的去处理。
除了并发问题,二级缓存还存在事务问题
二级缓存的逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| // -☆- CachingExecutor public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameterObject); // 创建 CacheKey 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,注意这里的 Cache // 并非是在 CachingExecutor 中创建的 Cache cache = ms.getCache(); // 如果配置文件中没有配置 <cache>,则 cache 为空 if (cache != null) { flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); // 访问二级缓存 List<E> list = (List<E>) tcm.getObject(cache, key); // 缓存未命中 if (list == null) { // 向一级缓存或者数据库进行查询 list = delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); // 缓存查询结果 tcm.putObject(cache, key, list); } return list; } } return delegate.<E>query( ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
|
注意二级缓存是从MappedStatement中获取的,而非由CachingExecutor创建。
由于 MappedStatement 存在于全局配置中,可以被多个CachingExecutor获取到,这样就会出现线程安全问题。除此之外,若不加以控制,多个事务共用一个缓存实例,会导致脏读问题。
- 线程安全问题可以通过SynchronizedCache装饰类解决,该装饰类会在Cache实例构造期间被添加上。
- 脏读问题,需要借助其他类来处理,也就是上面代码中 tcm 变量对应的类型。
事务缓存管理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| /** 事务缓存管理器 */ public class TransactionalCacheManager { // Cache 与 TransactionalCache 的映射关系表 private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>(); public void clear(Cache cache) { // 获取 TransactionalCache 对象,并调用该对象的 clear 方法,下同 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(); } } private TransactionalCache getTransactionalCache(Cache cache) { // 从映射表中获取 TransactionalCache TransactionalCache txCache = transactionalCaches.get(cache); if (txCache == null) { // TransactionalCache 也是一种装饰类,为 Cache 增加事务功能 txCache = new TransactionalCache(cache); transactionalCaches.put(cache, txCache); } return txCache; } }
|
TransactionalCacheManager 内部维护了Cache实例与 TransactionalCache实例间的映
射关系,该类也仅负责维护两者的映射关系,真正做事的还是TransactionalCache。
TransactionalCache 是一种缓存装饰器,可以为Cache实例增加事务功能。之前提到的
脏读问题正是由该类进行处理的。
TransactionalCache
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| public class TransactionalCache implements Cache { private final Cache delegate; private boolean clearOnCommit; // 在事务被ᨀ交前,所有从数据库中查询的结果将缓存在此集合中 private final Map<Object, Object> entriesToAddOnCommit; // 在事务被ᨀ交前,当缓存未命中时,CacheKey 将会被存储在此集合中 private final Set<Object> entriesMissedInCache; @Override public Object getObject(Object key) { // 查询 delegate 所代表的缓存 Object object = delegate.getObject(key); if (object == null) { // 缓存未命中,则将 key 存入到 entriesMissedInCache 中 entriesMissedInCache.add(key); } 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("..."); } } } }
|
在 TransactionalCache的代码中,我们要重点关注entriesToAddOnCommit集合,TransactionalCache 中的很多方法都会与这个集合打交道。
该集合用于存储从查询的结果,那为什么要将结果保存在该集合中,而非delegate 所表示的缓存中呢?
主要是因为直接存到delegate会导致脏数据问题。
脏数据问题发生的过程
假设两个线程开启两个不同的事务,它们的执行过程如下:
- 时刻 2,事务 A 对记录 A 进行了更新。
- 时刻 3,事务 A 从数据库查询记录A,并将记录 A 写入缓存中。
- 时刻 4,事务 B 查询记录A,由于缓存中存在记录A,事务B直接从缓存中取数据。
这个时候,脏数据问题就发生了。事务B在事务A未提交情况下,读取到了事务A所修改的记录。
为了解决这个问题,我们可以为每个事务引入一个独立的缓存。查询数据时,仍从 delegate缓存(以下统称为共享缓存)中查询。若缓存未命中,则查询数据库。
存储查询结果时,并不直接存储查询结果到共享缓存中,而是先存储到事务缓存中,也就是 entriesToAddOnCommit集合。当事务提交时,再将事务缓存中的缓存项转存到共享缓存中。
这样,事务 B 只能在事务A提交后,才能读取到事务A所做的修改,解决了脏读问题。整个过程大致如下:
- 时刻 2,事务A和B同时查询记录A。此时共享缓存中还没没有数据,所以两个事务均会向数据库发起查询请求,并将查询结果存储到各自的事务缓存中。
- 时刻 3,事务A更新记录A,这里把更新后的记录A记为A′。
- 时刻4,两个事务再次进行查询。此时,事务 A 读取到的记录为修改后的值,而事务 B 读取到的记录仍为原值。
- 时刻 5,事务 A 被提交,并将事务缓存 A 中的内容转存到共享缓存中。
- 时刻 6,事务 B 再次查询记录A,由于共享缓存中有相应的数据,所以直接取缓存数据即可。因此得到记录 A′,而非记录 A。但由于事务 A 已经提交,所以事务 B 读取到的记录 A′ 并非是脏数据。
MyBatis 引入事务缓存解决了脏读问题,事务间只能读取到其他事务提交后的内容,这相当于事务隔离级别中的“读已提交(ReadCommitted)”。但需要注意的时,MyBatis缓存事务机制只能解决脏读问题,并不能解决“不可重复读”问题。