Mybatis 缓存的实现原理

警告
本文最后更新于 2020-12-30,文中内容可能已过时。

mybatis源码系列文章,示例中带有中文注释的源码 copy 自 https://gitee.com/wlizhi/mybatis-3

链接中源码是作者从 github 下载,并以自身理解对核心流程及主要节点做了详细的中文注释。


1 缓存模块概要

  1. mybatis 的缓存模块是基于 Map 的,从缓存中读写数据是其基础功能。
  2. mybatis 提供了许多缓存的实现。如阻塞式缓存、先进先出缓存、日志缓存、缓存清空策略、软引用缓存、虚引用缓存、具有同步控制的缓存等。
  3. 这些缓存实现可以任意的组合优雅的附加到基础功能上。

如果使用继承、或动态代理的方式很难做到任意的组合(会有大量子类)。继承或动态代理方式在实现类的组合上来说是静态的,这是 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 中的元素计算的。

关键的成员变量:

  1. hashcode:CacheKey 的 hash 值,在 update 函数中实时运算出来的
  2. checksum:校验和,hash值的和
  3. count:参与计算元素的个数
  4. 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 的情况,所以存在一致性问题,一般不使用。