mybatis源码系列文章,示例中带有中文注释的源码 copy 自 https://gitee.com/wlizhi/mybatis-3
链接中源码是作者从 github 下载,并以自身理解对核心流程及主要节点做了详细的中文注释。
1 缓存模块概要
- mybatis 的缓存模块是基于 Map 的,从缓存中读写数据是其基础功能。
- mybatis 提供了许多缓存的实现。如阻塞式缓存、先进先出缓存、日志缓存、缓存清空策略、软引用缓存、虚引用缓存、具有同步控制的缓存等。
- 这些缓存实现可以任意的组合优雅的附加到基础功能上。
如果使用继承、或动态代理的方式很难做到任意的组合(会有大量子类)。继承或动态代理方式在实现类的组合上来说是静态的,这是 mybatis 缓存模块实现的最大难题。
使用装饰器模式可以解决此问题,mybatis 正是使用了装饰器模式实现的缓存模块。
2 装饰器模式
意图:动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。
主要解决:一般的,我们为了扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀。
何时使用:在不想增加很多子类的情况下扩展类。
如何解决:将具体功能职责划分,同时继承装饰者模式。
使用场景: 1、扩展一个类的功能。 2、动态增加功能,动态撤销。
jdk 中 io 流就是典型的装饰器模式。
类似这样:new BufferedInputStream(new FileInputStream("xx.txt"))
使用装饰类,持有原始类的方式,对其进行功能增强。如果需要多层的装饰,则创建装饰实例,然后作为参数传递给下一个装饰实例即可。
这样可以做到任意个数、任意顺序的装饰。装饰本身即是功能的增强。装饰类本身与原始类实现同一个接口/继承了同一个类。
上面示例中,BufferedInputStream、FileInputStream 实际上都实现了 InputStream。
Collections 类,对集合进行增强的一些静态方法,也是使用了装饰器模式。
3 缓存key的创建方式 - CacheKey
CacheKey 是对 mybatis 缓存的 key 值的封装。由于缓存时的 key 是根据多种元素来生成 key 的,这里需要通过一定的算法来进行封装,
生成 hashcode 和 equals。其中关键方法有 update()、hashCode()、equals()。
源码如下:
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
87
88
89
90
91
92
93
94
95
96
97
98
99
|
public class CacheKey implements Cloneable, Serializable {
private static final long serialVersionUID = 1146682552656046210L;
private static final int DEFAULT_MULTIPLIER = 37;
private static final int DEFAULT_HASHCODE = 17;
/** 参与 hash 计算的乘数 */
private final int multiplier;
/** CacheKey 的 hash 值,在 update 函数中运算出来的 */
private int hashcode;
/** 校验和,hash值的和 */
private long checksum;
/** 参与计算元素的个数 */
private int count;
// 8/21/2017 - Sonarlint flags this as needing to be marked transient. While true if content is not serializable, this
// is not always true and thus should not be marked transient.
/** 该集合中的元素决定两个 CacheKey 是否相等 */
private List<Object> updateList;
public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLIER;
this.count = 0;
this.updateList = new ArrayList<>();
}
public CacheKey(Object[] objects) {
this();
updateAll(objects);
}
public int getUpdateCount() {
return updateList.size();
}
public void update(Object object) {
// 获取object的hash值
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
// 更新count、checksum以及hashcode的值
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
// 将对象添加到updateList中
updateList.add(object);
}
public void updateAll(Object[] objects) {
for (Object o : objects) {
update(o);
}
}
@Override
public boolean equals(Object object) {
// 比较是不是同一个对象
if (this == object) {
return true;
}
// 是否类型相同
if (!(object instanceof CacheKey)) {
return false;
}
final CacheKey cacheKey = (CacheKey) object;
// hashcode 是否相同
if (hashcode != cacheKey.hashcode) {
return false;
}
// checksum 是否相同
if (checksum != cacheKey.checksum) {
return false;
}
// count是否相同
if (count != cacheKey.count) {
return false;
}
// 以上都相同,才按顺序比较 updateList 中元素是否相等,只有全部相等,才返回 true。
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;
}
@Override
public int hashCode() {
return hashcode;
}
}
|
CacheKey 是通过 MapperStatement 的id、offset、limit、以及 sql 中占位符的参数。来计算出对应的 hashcode,并且会将这些信息封装在一个 List 中。
在进行比较的时候,会先进行 hashcode、checksum 的比较,之后对封装的列表中的元素依次比较,如果全部相等,返回true,中途有一个比较不成立,则返回false。
这么做是为了提高效率。
hashcode 和 checkSum 的生成,是根据 updateList 中的元素计算的。
关键的成员变量:
- hashcode:CacheKey 的 hash 值,在 update 函数中实时运算出来的
- checksum:校验和,hash值的和
- count:参与计算元素的个数
- updateList:该集合中的元素决定两个 CacheKey 是否相等
cacheKey 的创建源码如下:
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 abstract class BaseExecutor implements Executor {
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 创建缓存 key
CacheKey cacheKey = new CacheKey();
// id、limit、sql等属性,计算 hash值,生成 cacheKey。
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
// 如果存在参数映射,将参数也加入cacheKey的生成元素中。
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
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;
}
}
|
4 一级缓存
mybatis 中的缓存是通过 Cache 接口来实现的。它负责存储缓存的 key-value 内容。
一级缓存的生命周期与 sqlSession 相同,即是线程隔离的。它的触发点是在二级缓存之后,也就是说,二级缓存中没有内容时,才会从一级缓存中获取,最后从数据库获取。
在 BaseExecutor 中有成员变量 localCache,这里就保存了一级缓存的内容。
默认的缓存实现类是 PerpetualCache,它通过 HashMap 来存储数据,其数据结构源码如下:
1
2
3
4
5
6
|
public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap<>();
}
|
如果我们调用一个查询语句,触发一级缓存的源码如下:
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
|
public abstract class BaseExecutor implements Executor {
@Override
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());
// 检查当前 executor 是否关闭。
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 非嵌套查询,并且 flushCacheRequired 配置为 true,则需要清空一级缓存。
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
// 查询层次 +1
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;
}
}
|
根据 cacheKey 从一级缓存中获取数据,如果存在,直接返回。如果不存在,则从数据库中查询数据。
5 二级缓存
二级缓存存储在 MappedStatement 的变量 cache 中,也就是与 Mapper 文件的封装类实例生命周期一致,是应用级的。
源码如下:
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
|
public class CachingExecutor implements Executor {
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 获取 sql 语句信息,包括占位符、参数等信息。
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 拼装缓存 key 值
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
// 执行查询
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 从 MappedStatement 中获取二级缓存
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) {
// 二级缓存为空,才会调用 BaseExecutor.query()。
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);
}
}
|
二级缓存和一级缓存 cacheKey 的生成并无区别。仅仅是存储的位置不同,导致其生命周期不同,这是他们最大的区别。
6 小结
一级缓存和二级缓存只是作用范围、生命周期不同。一级缓存是在sqlSession中,查询时保存到缓存、更新时清除缓存。二级缓存是在 MappedStatement 中,更新时会清除对应id的 MappedStatement 中的二级缓存。
二级缓存可能存在跨 MappedStatement 的情况,所以存在一致性问题,一般不使用。