二级缓存构建在一级缓存之上,在收到查询请求时,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会导致脏数据问题。

脏数据问题发生的过程

假设两个线程开启两个不同的事务,它们的执行过程如下:
image.png

  • 时刻 2,事务 A 对记录 A 进行了更新。
  • 时刻 3,事务 A 从数据库查询记录A,并将记录 A 写入缓存中。
  • 时刻 4,事务 B 查询记录A,由于缓存中存在记录A,事务B直接从缓存中取数据。

这个时候,脏数据问题就发生了。事务B在事务A未提交情况下,读取到了事务A所修改的记录。

为了解决这个问题,我们可以为每个事务引入一个独立的缓存。查询数据时,仍从 delegate缓存(以下统称为共享缓存)中查询。若缓存未命中,则查询数据库。

存储查询结果时,并不直接存储查询结果到共享缓存中,而是先存储到事务缓存中,也就是 entriesToAddOnCommit集合。当事务提交时,再将事务缓存中的缓存项转存到共享缓存中。

这样,事务 B 只能在事务A提交后,才能读取到事务A所做的修改,解决了脏读问题。整个过程大致如下:
image.png

  • 时刻 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缓存事务机制只能解决脏读问题,并不能解决“不可重复读”问题。