前面提到的三个基础执行器都是需要和数据库进行直接交互的。
mybatis 本身有两层缓存结构。部分情况下的查询操作,可能并不会请求数据库,而是通过框架提供的两级缓存就完成了处理并返回。
一 BaseExecutor
基础执行器,这一层级包含了 mybatis 两级缓存中的第一级缓存。
mybatis 默认开启第一级缓存。
其次,执行器的设计分层遵循了软件设计的 单一职责 原则。BaseExecutor 只管理一级缓存,而具体的数据库交互逻辑,是交由更低层的三个执行器处理的(Simple/Reuse/Batch)。
1.1 构造函数
protected BaseExecutor(Configuration configuration, Transaction transaction) {
// 事物对象
this.transaction = transaction;
// 延迟加载队列
this.deferredLoads = new ConcurrentLinkedQueue<>();
// 一级缓存
this.localCache = new PerpetualCache("LocalCache");
// 本地输出参数缓存
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
// 执行器状态标识对象
this.closed = false;
// mybatis 配置对象
this.configuration = configuration;
this.wrapper = this;
}
一级缓存就是构造中的 localCache,可以看到是 mybatis 自定义对象
org.apache.ibatis.cache.impl.PerpetualCache
点开看一下,可以知道其实还是对 hashmap 的一个包装。除此之外新增的类成员是 id。类中复写了 equals 方法,看到这部分代码,即可知道这个 id 字段的作用,是用来标识全局范围内的唯一性。两个不同的 PerpetualCache 对象的检查,就是依赖 id 字段识别。构造函数中创建了两个 PerpetualCache,构造入参就是用来识别的 id。
1.2 update 操作
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
// 执行器状态检查
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 清除一级缓存
clearLocalCache();
// 执行更新操作
return doUpdate(ms, parameter);
}
一级缓存的作用域是 Session 级别,即会话层级,当 session 域内执行更新操作的时候,会直接清空一级缓存,没有做比较细的区分处理。比如 update 操作更新的是表 b,而一级缓存保有了表 a 的查询结果,但是此时更新表 b,一级缓存中关于表 a 的内容也会被清空。
- 坏处是这样导致一级缓存的保存时间短,命中率降低
- 好处是该级缓存管理难度低,刷新率高也会让缓存的空间占用保持在一个较低的水平
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
清除缓存操作,处理了两个类成员
- localCache
- localOutputParameterCache
这俩就是一级缓存的组成。
protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;
下面执行的更新操作,是在抽象类中定义的抽象方法,具体实现,则落到了前面提到过的三个基础执行器上面。这里就是 单一职责 原则的体现。BaseExecutor 只负责处理一级缓存,具体和数据库交互的逻辑,被交由基础执行器。
1.3 query 操作
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());
// 检查执行器状态
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 如果查询堆栈为0(即没有递归调用),清除缓存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
// 执行查询逻辑
try {
// 查询堆栈记录值 +1
queryStack++;
// 根据 cacheKey 检查是否命中缓存
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--;
}
if (queryStack == 0) {
// 查询堆栈为0,检查延迟加载堆栈中的数据并加载
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// 清空延迟加载数据
// issue #601
deferredLoads.clear();
// 如果一级缓存的作用域被设置到 stmt 级别,就在执行完查询后清空一级缓存
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
}
session 级别调用到 query 是这个方法。如果只是简单基础的查询逻辑的话,这个方法本不会这么复杂。但是涉及到了存储过程/包含嵌套查询的复杂 sql 等场景。所以衍生出来了较多的分支逻辑。
如下的字符串,是查询方法中,检索一级缓存是否命中时传入的关键参数 cachekey。这个 cachekey 的组成并不是很简单的 namespace+id 拼凑而成。而是杂糅了诸多因素进去。这些组成因子也因此可以决定一级缓存是否可以命中。
1298872468:4918445298:com.gaop.mapper.WaterMapper.getById:0:2147483647:select * from water_00 where id = ?:1:development
1.4 缓存 key 的组成与一级缓存的命中条件
在方法
PerpetualCache.get(Object key)
中,实际是执行了 map.get() 方法。所以检索 key 是否匹配,检查的是传入对象的 equals 方法。而 mybatis 传入的 key,固定了类型为 CacheKey。
创建一个 cacheKey 的流程需要聚焦到另一个方法
org.apache.ibatis.executor.BaseExecutor#createCacheKey
这个方法是用于创建以及填充一个完整的 cacheKey 对象的执行流程。
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 初始化缓存 key 对象,这个对象有三个默认参数值 hashcode-17,multiplier-37,count-0
CacheKey cacheKey = new CacheKey();
// 依次加入 namespaceid/mybatis分页参数/绑定的sql,用于生成哈希码
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
// 最后处理sql入参内容,并将处理后的入参加入 hashcode 生成
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);
}
}
// 这里一个补充因子,mybatis 环境配置,不同环境的 sql 不能混在一起
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
整个创建逻辑的核心内容在于 hashcode 的生成。 每一个加入 hashcode 生成流程的参数都会影响到一级缓存是否命中。 我们可以总结一下有那些因子:
- namespaceid
- mybatis 分页参数 limite/offset
- sql
- sql 参数
- myabtis 环境参数
创建完一个 cacheKey 之后,需要看一下比较一个 cacheKey 的核心逻辑。因为一级缓存用的就是用这个 cacheKey 作为 hashmap 的 key 值。
如下的内容为
org.apache.ibatis.cache.CacheKey#equals
的方法内容,也就是一级缓存是否命中的检查逻辑执行的核心内容。
public boolean equals(Object object) {
// 如果比较对象是自己,通过
if (this == object) {
return true;
}
// 如果传入的 key 的类型不是 cacheKey,则不通过
if (!(object instanceof CacheKey)) {
return false;
}
// 对象类型转换
final CacheKey cacheKey = (CacheKey) object;
// 比较类成员哈希码值是否匹配,不匹配则不通过
if (hashcode != cacheKey.hashcode) {
return false;
}
// 交验参数和是否匹配,不匹配则不通过
if (checksum != cacheKey.checksum) {
return false;
}
// 如果是更新类型操作,会传入入参参数总数,这个值不匹配也不会通过
if (count != cacheKey.count) {
return false;
}
// updateList 是按顺序加入生成 hashcode 的参数列表,hashcode 相等不意味着传入的参数完全一致,所以这里需要比较生存 hashcode 参数的列表。如果出现不一致的,则不通过。
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;
}
其实一段 hashcode 的代码检查逻辑,整体思路就和 hashmap 里检索 key 的思路很相似了。先检查 hashcode 是否相等,再检查组成 hashcode 的数据是否相等。
二 简要总结
BaseExecutor 处于 mybatis 执行器体系的中间层。其设计与实现遵循了 单一职责 原则。具体体现在 baseExecutor 主要聚焦在处理一级缓存的逻辑上,而与数据库交互的具体实现依赖于另外三个底层执行器(simple/reuse/batch)。
一级缓存默认是开启的,作用域默认会话层级(session)。任意更新操作都会清空一级缓存中的所有数据。一级缓存有一个 mybatis 的自定义实现类
org.apache.ibatis.cache.impl.PerpetualCache
但是其内容也是对 hashmap 的一个包装。所以核心数据结构还是 hashmap
该缓存 key 的生成逻辑依赖了一些列的参数,这些参数也就是影响一级缓存命中的因素列表。有一个专门的类用于保存这些数据
org.apache.ibatis.cache.CacheKey
影响一级缓存命中的因素有
- namespaceid
- mybatis 分页参数 limit/offset
- sql
- sql 入参
- mybatis 环境配置参数