MyBatis 核心处理层:SQL 执行流程

MyBatis SQL 语句执行的核心接口是 Executor。在 Executor 接口的实现过程中,MyBatis 使用了装饰器模式模板方法模式两种经典的设计模式。

1 模板方法模式

在我们开发业务逻辑的时候,可能会遇到流程复杂的逻辑,而这个复杂逻辑本身是可以拆解成多个小的行为,这些小的行为本身可能根据业务场景的不同而有所变化。

这里我们以转账流程为例,如下图所示,整个转账流程是固定的,但是“验证密码”“验证余额”和“扣除金额”这三步针对不同的银行卡,要调用不同银行的接口去完成。

模板方法模式

为了让整个复杂流程的代码具有更好的扩展性,我们一般会使用模板方法模式来处理。

在模板方法模式中,我们可以将复杂流程中每个步骤的边界确定下来,然后由一个“模板方法”定义每个步骤的执行流程,每个步骤对应着一个方法,这些方法也被称为“基本方法”。模板方法按照业务逻辑依次调用上述基本方法,来实现完整的复杂流程。

模板方法模式会将模板方法以及不需要随业务场景变化的基本方法放到父类中实现,随业务场景变化的基本方法会被定义为抽象方法,由子类提供真正的实现。

下图展示了模板方法模式的核心类,其中 template() 方法是我们上面描述的模板方法,part1() 方法和 part3() 方法是逻辑不变的基本方法实现,而 part2()part4() 方法是两个随场景变化的基本方法。

模板方法模式示意图

我们可以通过模板方法控制整个流程的走向以及其中固定不变的步骤,子类来实现流程的某些变化细节,这就实现了“变化与不变”的解耦,也实现了“整个流程与单个步骤”的解耦。当业务需要改变流程中某些步骤的具体行为时,直接添加新的子类即可实现,这也非常符合“开放 —— 封闭”原则。另外,模板方法模式能够充分利用面向对象的多态特性,在系统运行时再选择一种具体子类来执行完整的流程,这也从另一个角度提高了系统的灵活性。

2 Executor 接口

首先来看 Executor 接口定义的核心方法,Executor 接口定义了数据库操作的基本方法,如下图所示:

Executor 接口结构图

其中 queryXxx() 方法、update() 方法、flushStatement() 方法是执行 SQL 语句的基础方法,commit() 方法、rollback() 方法以及 getTransaction() 方法与事务的提交/回滚相关,clearLocalCache() 方法、createCacheKey() 方法与缓存有关。

MyBatis 中有多个 Executor 接口的实现类,如下图所示:

Executor 接口继承关系图
  • 该图中的 CachingExecutorExecutor 的装饰器实现,在其他 Executor 实现的基础上添加了缓存的功能;
  • BaseExecutor 实现了 Executor 接口的全部方法,主要定义了这些方法的核心流程(也就是模板方法),然后由子类进行具体实现。

3 BaseExecutor

BaseExecutor 使用模板方法模式实现了 Executor 接口中的方法,其中,不变的部分是事务管理和缓存管理两部分的内容,由 BaseExecutor 实现;变化的部分则是具体的数据库操作,由 BaseExecutor 子类实现,涉及 doUpdate()doQuery()doQueryCursor()doFlushStatement() 这四个方法。

下面我们会从缓存和事务两个角度来讲解 BaseExecutor 的核心实现。

3.1 一级缓存

数据库作为 OLTP 系统中的核心资源之一,是性能优化的重点关注对象,在设计、开发以及后期运维时,我们都会采取多种手段减少数据库压力,其中使用缓存是一种比较常用且有效的优化数据库读写效率的手段。

很多持久层框架默认都提供了基于 JVM 堆内存的缓存实现,MyBatis 也不例外。MyBatis 缓存分为一级缓存和二级缓存,这里我们先重点来看一级缓存的内容。

MyBatis 中的一级缓存是会话级缓存,创建一个 SqlSession 对象就表示开启一次与数据库的会话,会话生命周期与 SqlSession 的生命周期一致。在一次会话中,我们可能多次执行相同的查询语句,如果没有缓存,每次查询都会请求到数据库,这样就会浪费数据库资源。

为了避免上述资源浪费问题,BaseExecutor 会给每个 SqlSession 对象关联一个 Cache 对象,也就是“一级缓存”。在使用 SqlSession 对象进行查询的时候,会先访问一级缓存,看看是否已经缓存了结果对象,如果存在,则直接返回一级缓存中的结果对象,这也就是我们常说的“命中缓存”。如果未命中缓存,则会击穿到数据库,一级缓存会将数据库返回的查询结果对象缓存起来,等待后续请求使用。MyBatis 中的一级缓存默认处于开启状态,也推荐用户开启一级缓存。

下面来看 BaseExecutor 与一级缓存的相关实现。在 BaseExecutor 中维护了两个 PerpetualCache 对象,分别是 localCache 字段和 localOutputParameterCache 字段,其中 localOutputParameterCache 只用来缓存存储过程的输出参数,localCache 会用来缓存其他查询方式的结果对象。

BaseExecutor.query() 方法中,定义了查询操作的核心流程,其中也包含了查询一级缓存和填充一级缓存的操作,其具体核心步骤如下:

  • 创建 CacheKey 对象,该部分逻辑在 createCacheKey() 方法中实现。这里创建的 CacheKey 对象主要包含五个部分:
    • MappedStatementid;
    • RowBounds 中的 offsetlimit 信息;
    • SQL 语句(包含 ? 占位符);
    • 用户传递的实参信息;
    • Environment ID。
  • 使用 CacheKey 查询一级缓存。如果缓存命中,则直接返回缓存的结果对象;如果缓存未命中,则调用 doQuery() 方法完成数据库查询操作,同时将结果对象记录到一级缓存中。
  • 除了上述查询缓存、数据库等操作之外,query() 方法最后还会处理嵌套查询的缓存。在这一步中,BaseExecutor 会遍历全部嵌套查询对应的 DeferredLoad 对象,并通过 load() 方法从 localCache 中获取嵌套查询的对象,填充到外层对象的相应属性中。

下面来看 query() 方法的核心逻辑:

 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
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.");
  }
  if (queryStack == 0 && ms.isFlushCacheRequired()) {
    // 非嵌套查询,并且<select>标签配置的flushCache属性为true时,才会清空一级缓存
    // 注意:flushCache配置项会影响一级缓存中结果对象存活时长
    clearLocalCache();
  }
  List<E> list;
  try {
    queryStack++;// 增加查询层数
    // 查询一级缓存
    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    if (list != null) {
      // 对存储过程出参的处理:如果命中一级缓存,则获取缓存中保存的输出参数,
      // 然后记录到用户传入的实参对象中
      handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    } else {
      // queryFromDatabase()方法内部首先会在localCache中设置一个占位符,
      // 然后调用doQuery()方法完成数据库查询,并得到映射后的结果对象,
      // doQuery()方法是一个抽象方法,由BaseExecutor的子类具体实现
      list = queryFromDatabase(ms, parameter, rowBounds, resultHandler,
       key, boundSql);
    }
  } finally {
    // 当前查询完成,查询层数减少
    queryStack--;
  }
  if (queryStack == 0) { // 完成嵌套查询的填充
    for (DeferredLoad deferredLoad : deferredLoads) {
      deferredLoad.load();
    }
    // 清空deferredLoads集合
    deferredLoads.clear();
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
      // 根据配置决定是否清空localCache
      clearLocalCache();
    }
  }
  return list;
}

通过对 query() 这个核心方法的分析,我们可以看到其中有两处影响一级缓存中结果对象生命周期的配置:

  • 一个是 <select> 标签的 flushCache 配置,它决定了一条 SELECT 语句执行之前是否会清除一级缓存;
  • 另一个是全局的 localCacheScope 配置,它决定了一级缓存的生命周期是语句级别的(STATEMENT)还是 SqlSession 级别的(SESSION),默认值是 SqlSession 级别的。

除了上述两个配置会影响缓存数据的生命周期之外,修改操作也会清空缓存,涉及 commit()rollback()update() 方法:

localCacheScope

为了保持一级缓存与数据库的一致性,这些修改数据的操作需要清空一级缓存,因为执行修改操作之后,数据库中存储的数据已更新,如果一级缓存的内容不更新的话,就会与数据库中的数据不一致,成为“脏数据”。

3.2 事务管理

现在我们知道 commit()rollback() 方法在提交和回滚事务之前会清空一级缓存,这里我们简单介绍一下 BaseExecutor 事务管理相关的内容。

BaseExecutor 中维护了一个 Transaction 对象(transaction 字段)来控制事务。首先来看 getConnection() 方法,它底层会通过 Transaction.getConnection() 方法获取数据库连接,用于创建 StatementPreparedStatement 等对象。

再来看 commit() 方法和 rollback() 方法,分别依赖 Transaction.commit() 方法和 Transaction.rollback() 方法来提交和回滚事务。从 commit() 方法和 rollback() 方法中我们可以看到,在清理一级缓存和提交/回滚事务之间,BaseExecutor 还会执行 flushStatements() 方法,这个方法主要是处理批处理场景,其中会调用 doFlushStatements() 来处理通过 batch() 写入的多条 SQL 语句。

4 SimpleExecutor

SimpleExecutorBaseExecutor 的一个子类,同时也是 Executor 接口最简单的实现。

正如上文中分析的那样,BaseExecutor 通过模板方法模式实现了读写一级缓存、事务管理等不随场景变化的基础方法,在 SimpleExecutorReuseExecutorBatchExecutor 等实现类中,不再处理这些不变的逻辑,而只要关注 4 个 doXxx() 方法的实现即可。

这里我们重点来看 doQuery() 方法的实现逻辑:

  1. 通过 newStatementHandler() 方法创建 StatementHandler 对象,其中会根据 MappedStatement.statementType 配置创建相应的 StatementHandler 实现对象,并添加 RoutingStatementHandler 装饰器。
  2. 通过 prepareStatement() 方法初始化 Statement 对象,其中还依赖 ParameterHandler 填充 SQL 语句中的占位符。
  3. 通过 StatementHandler.query() 方法执行 SQL 语句,并通过我们前面介绍的 DefaultResultSetHandlerResultSet 映射成结果对象并返回。

doQuery() 方法的核心代码实现如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public <E> List<E> doQuery(MappedStatement ms, Object parameter, 
    RowBounds rowBounds, ResultHandler resultHandler, 
    BoundSql boundSql) throws SQLException {
  Statement stmt = null;
  try {
    Configuration configuration = ms.getConfiguration();
    // 创建StatementHandler对象,实际返回的是RoutingStatementHandler对象
    // 其中根据MappedStatement.statementType选择具体的StatementHandler实现
    StatementHandler handler = configuration.newStatementHandler(wrapper, 
      ms, parameter, rowBounds, resultHandler, boundSql);
    // 完成StatementHandler的创建和初始化,该方法会调用StatementHandler.prepare()方法创建
    // Statement对象,然后调用StatementHandler.parameterize()方法处理占位符
    stmt = prepareStatement(handler, ms.getStatementLog());
    // 调用StatementHandler.query()方法,执行SQL语句,
    // 并通过ResultSetHandler完成结果集的映射
    return handler.query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}

SimpleExecutor 中的 doQueryCursor()update() 等方法实现与 doQuery() 方法的实现基本类似。

5 ReuseExecutor

重用 Statement 对象是一种常见的优化手段,主要目的是减少 SQL 预编译开销,同时还会降低 Statement 对象的创建和销毁频率,这在一定程度上可以提升系统性能。

ReuseExecutor实现了重用 Statement 的优化ReuseExecutor 维护了一个 statementMap 字段(HashMap<String, Statement> 类型)来缓存已有的 Statement 对象,该缓存的 Key 是 SQL 模板,Value 是 SQL 模板对应的 Statement 对象。这样在执行相同 SQL 模板时,我们就可以复用 Statement 对象了。

ReuseExecutor 中的 doXxx() 方法实现与前面介绍的 SimpleExecutor 实现完全一样,两者唯一的区别在于其中依赖的 prepareStatement() 方法SimpleExecutor 每次都会创建全新的 Statement 对象,ReuseExecutor 则是先尝试查询 statementMap 缓存,如果缓存命中,则会重用其中的 Statement 对象。

另外,在事务提交/回滚以及 Executor 关闭的时候,需要同时关闭 statementMap 集合中缓存的全部 Statement 对象,这部分逻辑是在 doFlushStatements() 方法中实现的,核心代码如下:

1
2
3
4
5
6
7
8
9
public List<BatchResult> doFlushStatements(boolean isRollback) {
  // 关闭statementMap集合中缓存的全部Statement对象
  for (Statement stmt : statementMap.values()) {
    closeStatement(stmt);
  }
  // 清空statementMap集合
  statementMap.clear();
  return Collections.emptyList();
}

6 BatchExecutor

批处理是 JDBC 编程中的另一种优化手段。JDBC 在执行 SQL 语句时,会将 SQL 语句以及实参通过网络请求的方式发送到数据库,一次执行一条 SQL 语句,一方面会减小请求包的有效负载,另一个方面会增加耗费在网络通信上的时间。通过批处理的方式,我们就可以在 JDBC 客户端缓存多条 SQL 语句,然后在 flush 或缓存满的时候,将多条 SQL 语句打包发送到数据库执行,这样就可以有效地降低上述两方面的损耗,从而提高系统性能。

不过,有一点需要特别注意:每次向数据库发送的 SQL 语句的条数是有上限的,如果批量执行的时候超过这个上限值,数据库就会抛出异常,拒绝执行这一批 SQL 语句,所以我们需要控制批量发送 SQL 语句的条数和频率。

BatchExecutor 是用于实现批处理的 Executor 实现,其中维护了一个 List<Statement> 集合(statementList 字段)用来缓存一批 SQL,每个 Statement 可以写入多条 SQL。

JDBC 的批处理操作只支持 insertupdatedelete 等修改操作,也就是说 BatchExecutor 对批处理的实现集中在 doUpdate() 方法中。在 doUpdate() 方法中追加一条待执行的 SQL 语句时,BatchExecutor 会先将该条 SQL 语句与最近一次追加的 SQL 语句进行比较,如果相同,则追加到最近一次使用的 Statement 对象中;如果不同,则追加到一个全新的 Statement 对象,同时会将新建的 Statement 对象放入 statementList 缓存中。

下面是 BatchExecutor.doUpdate() 方法的核心逻辑:

 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
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
  final Configuration configuration = ms.getConfiguration();
  // 创建StatementHandler对象
  final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);
  final BoundSql boundSql = handler.getBoundSql();
  // 获取此次追加的SQL模板
  final String sql = boundSql.getSql();
  final Statement stmt;
  // 比较此次追加的SQL模板与最近一次追加的SQL模板,以及两个MappedStatement对象
  if (sql.equals(currentSql) && ms.equals(currentStatement)) {
    // 两者相同,则获取statementList集合中最后一个Statement对象
    int last = statementList.size() - 1;
    stmt = statementList.get(last);
    applyTransactionTimeout(stmt);
    handler.parameterize(stmt);// 设置实参
    // 查找该Statement对象对应的BatchResult对象,并记录用户传入的实参
    BatchResult batchResult = batchResultList.get(last);
    batchResult.addParameterObject(parameterObject);
  } else {
    Connection connection = getConnection(ms.getStatementLog());
    // 创建新的Statement对象
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);// 设置实参
    // 更新currentSql和currentStatement
    currentSql = sql;
    currentStatement = ms;
    // 将新创建的Statement对象添加到statementList集合中
    statementList.add(stmt);
    // 为新Statement对象添加新的BatchResult对象
    batchResultList.add(new BatchResult(ms, sql, parameterObject));
  }
  handler.batch(stmt);
  return BATCH_UPDATE_RETURN_VALUE;
}

这里使用到的 BatchResult 用于记录批处理的结果,一个 BatchResult 对象与一个 Statement 对象对应,BatchResult 中维护了一个 updateCounts 字段(int[] 数组类型)来记录关联 Statement 对象执行批处理的结果。

添加完待执行的 SQL 语句之后,我们再来看一下 doFlushStatements() 方法,其中会通过 Statement.executeBatch() 方法批量执行 SQL,然后 SQL 语句影响行数以及数据库生成的主键填充到相应的 BatchResult 对象中返回。下面是其核心实现:

 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
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
  try {
    // 用于储存批处理的结果
    List<BatchResult> results = new ArrayList<>();
    // 如果明确指定了要回滚事务,则直接返回空集合,忽略statementList集合中记录的SQL语句
    if (isRollback) {
      return Collections.emptyList();
    }
    for (int i = 0, n = statementList.size(); i < n; i++) {
      Statement stmt = statementList.get(i);
      applyTransactionTimeout(stmt);
      BatchResult batchResult = batchResultList.get(i);
      try {
        // 调用Statement.executeBatch()方法批量执行其中记录的SQL语句,并使用返回的int数组
        // 更新BatchResult.updateCounts字段,其中每一个元素都表示一条SQL语句影响的记录条数
        batchResult.setUpdateCounts(stmt.executeBatch());
        MappedStatement ms = batchResult.getMappedStatement();
        List<Object> parameterObjects = batchResult.getParameterObjects();
        // 获取配置的KeyGenerator对象
        KeyGenerator keyGenerator = ms.getKeyGenerator();
        if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
          // 获取数据库生成的主键,并记录到实参中对应的字段
          Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
          jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
        } else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) {
          // 其他类型的KeyGenerator,会调用其processAfter()方法
          for (Object parameter : parameterObjects) {
            keyGenerator.processAfter(this, ms, stmt, parameter);
          }
        }
        // Close statement to close cursor #1109
        closeStatement(stmt);
      } catch (BatchUpdateException e) {
        // 异常处理逻辑
      }
      // 添加BatchResult到results集合
      results.add(batchResult);
    }
    return results;
  } finally {
    // 释放资源逻辑
  }
}

7 CachingExecutor

CachingExecutor 是一个 Executor 装饰器实现,会在其他 Executor 的基础之上添加二级缓存的相关功能。

7.1 二级缓存

我们知道一级缓存的生命周期默认与 SqlSession 相同,而这里介绍的 MyBatis 中的二级缓存则与应用程序的生命周期相同。与二级缓存相关的配置主要有下面三项:

  1. 二级缓存全局开关:这个全局开关是核心配置文件中的 cacheEnabled 配置项。当 cacheEnabled 被设置为 true 时,才会开启二级缓存功能,开启二级缓存功能之后,下面两项的配置才会控制二级缓存的行为。
  2. 命名空间级别开关:在 Mapper 配置文件中,可以通过配置 <cache> 标签或 <cache-ref> 标签开启二级缓存功能。
  • 在解析到 <cache> 标签时,MyBatis 会为当前 Mapper.xml 文件对应的命名空间创建一个关联的 Cache 对象(默认为 PerpetualCache 类型的对象),作为其二级缓存的实现。此外,<cache> 标签中还提供了一个 type 属性,我们可以通过该属性使用自定义的 Cache 类型。
  • 在解析到 <cache-ref> 标签时,MyBatis 并不会创建新的 Cache 对象,而是根据 <cache-ref> 标签的 namespace 属性查找指定命名空间对应的 Cache 对象,然后让当前命名空间与指定命名空间共享同一个 Cache 对象。
  1. 语句级别开关:我们可以通过 <select> 标签中的 useCache 属性,控制该 select 语句查询到的结果对象是否保存到二级缓存中,useCache 属性默认值为 true。

7.2 TransactionalCache

了解了二级缓存的生命周期、基本概念以及相关配置之后,我们开始介绍 CachingExecutor 依赖的底层组件。

CachingExecutor 底层除了依赖 PerpetualCache 实现来缓存数据之外,还会依赖 TransactionalCacheTransactionalCacheManager 两个组件,下面我们就一一详细介绍下。

TransactionalCacheCache 接口众多实现之一,它也是一个装饰器,用来记录一个事务中添加到二级缓存中的缓存。

TransactionalCache 中的 entriesToAddOnCommit 字段(Map<Object, Object> 类型)用来暂存当前事务中添加到二级缓存中的数据,这些数据在事务提交时才会真正添加到底层的 Cache 对象(也就是二级缓存)中。这一点我们可以从 TransactionalCacheputObject() 方法以及 flushPendingEntries() 方法(commit() 方法会调用该方法)中看到相关代码实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public void putObject(Object key, Object object) {
  // 将数据暂存到entriesToAddOnCommit集合
  entriesToAddOnCommit.put(key, object);
}
private void flushPendingEntries() {
  for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
    // 将entriesToAddOnCommit集合中的数据添加到二级缓存
    delegate.putObject(entry.getKey(), entry.getValue());
  }
  // 缓存未命中逻辑...
}

注释:

为什么要在事务提交时才将 entriesToAddOnCommit 集合中的缓存数据写入底层真正的二级缓存中,而不是像操作一级缓存那样,每次查询都直接写入缓存呢?其实这是为了防止出现“脏读”。

我们假设当前数据库的隔离级别是“不可重复读”,如下图所示,两个业务线程分别开启了 T1、T2 两个事务:

  • 在事务 T1 中添加了记录 A,之后查询记录 A;
  • 事务 T2 会查询两次记录 A。

两事务并发操作的示意图:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
开启事务 T1                      提交事务 T1
x------------事务 T1 的跨度-----------x
|===================================|
  |      ^         ^                |
  |      |         |                |
  |  添加记录A  查询记录A           
  |                   查询记录A      查询记录A
  |                      |          |   |
  |                      v          |   v
  |=======================================|
  x--------------事务 T2 的跨度-------------x
开启事务 T2                        提交事务 T2

如果事务 T1 查询记录 A 时,就将 A 对应的结果对象写入二级缓存,那在事务 T2 查询记录 A 时,会从二级缓存中直接拿到结果对象。此时的事务 T1 仍然未提交,也就出现了“脏读”。

我们按照 TransactionalCache 的实现再来分析下,事务 T1 查询 A 数据的时候,未命中二级缓存,请求穿透到数据库,因为写入和读取 A 都是在事务 T1 中,所以能够查询成功,同时更新 entriesToAddOnCommit 集合。事务 T2 查询记录 A 时,同样也会绕过二级缓存访问数据库,因为此时写入和读取 A 是不同的事务,且数据库的事务隔离级别为“不可重复读”,这就导致事务 T2 无法查询到记录 A,也就避免了“脏读”。

如上图所示,事务 T1 在提交时,会将 entriesToAddOnCommit 中的数据添加到二级缓存中,所以事务 T2 第二次查询记录 A 时,会命中二级缓存,也就出现了同一事务中多次读取的结果不同的现象,也就是我们说的“不可重复读”。

TransactionalCache 中的另一个核心字段是 entriesMissedInCache,它用来记录未命中的 CacheKey 对象。在 getObject() 方法中,我们可以看到写入 entriesMissedInCache 集合的相关代码片段:

1
2
3
4
5
6
7
public Object getObject(Object key) {
  Object object = delegate.getObject(key);
  if (object == null) {
    entriesMissedInCache.add(key);
  }
  // 其他逻辑
}

在事务提交的时候,会将 entriesMissedInCache 集合中的 CacheKey 写入底层的二级缓存(写入时的 Value 为 null)。在事务回滚时,会调用底层二级缓存的 removeObject() 方法,删除 entriesMissedInCache 集合中 CacheKey

注释:

为什么要用 entriesMissedInCache 集合记录未命中缓存的 CacheKey 呢?为什么还要在缓存结束时删除这些 CacheKey 呢?

这主要是与之前介绍的 BlockingCache 装饰器有关。在前面介绍 Cache 时我们提到过,CacheBuilder 默认会添加 BlockingCache 这个装饰器,而 BlockingCachegetObject() 方法会有给 CacheKey 加锁的逻辑,需要在 putObject() 方法或 removeObject() 方法中解锁,否则这个 CacheKey 会被一直锁住,无法使用。

7.3 TransactionalCacheManager

了解了 TransactionalCache 的核心实现之后,我们再来看 TransactionalCache 的管理者 —— TransactionalCacheManager,其中定义了一个 transactionalCaches 字段(HashMap<Cache, TransactionalCache> 类型)维护当前 CachingExecutor 使用到的二级缓存,该集合的 Key 是二级缓存对象,Value 是装饰二级缓存的 TransactionalCache 对象。

TransactionalCacheManager 中的方法实现都比较简单,都是基于 transactionalCaches 集合以及 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
public class TransactionalCacheManager {

  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();

  public void clear(Cache cache) {
    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) {
    return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
  }

}

7.4 CachingExecutor 核心实现

了解了二级缓存基本概念以及 TransactionalCache 核心实现之后,我们再来看 CachingExecutor 的核心实现。

CachingExecutor 作为一个装饰器,其中自然会维护一个 Executor 类型字段指向被装饰的 Executor 对象,同时它还创建了一个 TransactionalCacheManager 对象来管理使用到的二级缓存。

CachingExecutor 的核心在于 query() 方法,其核心操作大致可总结为如下:

  • 获取 BoundSql 对象,创建查询语句对应的 CacheKey 对象。
  • 尝试获取当前命名空间使用的二级缓存,
    • 如果没有指定二级缓存,则表示未开启二级缓存功能,此时直接使用被装饰的 Executor 对象进行数据库查询操作。
    • 如果开启了二级缓存功能,则继续后面的步骤。
      • 查询二级缓存,这里使用到 TransactionalCacheManager.getObject() 方法,
        • 如果二级缓存命中,则直接将该结果对象返回。
        • 如果二级缓存未命中,则通过被装饰的 Executor 对象进行查询。最后,会将查询到的结果对象放入 TransactionalCache 中的 entriesToAddOnCommit 集合里暂存,等待事务提交时再写入二级缓存。

下面是 CachingExecutor.query() 方法的核心代码片段:

 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
public <E> List<E> query(MappedStatement ms, 
    Object parameterObject, RowBounds rowBounds, 
    ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
  Cache cache = ms.getCache();// 获取该命名空间使用的二级缓存
  if (cache != null) {// 是否开启了二级缓存功能
    flushCacheIfRequired(ms);// 根据<select>标签配置决定是否需要清空二级缓存
    // 检测useCache配置以及是否使用了resultHandler配置
    if (ms.isUseCache() && resultHandler == null) {
      // 是否包含输出参数
      ensureNoOutParams(ms, boundSql);
      // 查询二级缓存
      @SuppressWarnings("unchecked")
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {
        // 二级缓存未命中,通过被装饰的Executor对象查询结果对象
        list = delegate.query(ms, parameterObject, 
          rowBounds, resultHandler, key, boundSql);
        // 将查询结果放入TransactionalCache.entriesToAddOnCommit集合中暂存
        tcm.putObject(cache, key, list);
      }
      return list;
    }
  }
  // 如果未开启二级缓存,直接通过被装饰的Executor对象查询结果对象
  return delegate.query(ms, parameterObject, rowBounds, 
    resultHandler, key, boundSql);
}


欢迎关注我的公众号,第一时间获取文章更新:

微信公众号

相关内容