mybatis-executor
1. 概述
从本文开始,我们来分享 SQL 执行的流程。在 《精尽 MyBatis 源码分析 —— 项目结构一览》 中,我们简单介绍这个流程如下:
对应
executor和cursor模块。前者对应执行器,后者对应执行结果的游标。SQL 语句的执行涉及多个组件 ,其中比较重要的是 Executor、StatementHandler、ParameterHandler 和 ResultSetHandler 。
- Executor 主要负责维护一级缓存和二级缓存,并提供事务管理的相关操作,它会将数据库相关操作委托给 StatementHandler完成。
- StatementHandler 首先通过 ParameterHandler 完成 SQL 语句的实参绑定,然后通过
java.sql.Statement对象执行 SQL 语句并得到结果集,最后通过 ResultSetHandler 完成结果集的映射,得到结果对象并返回。整体过程如下图:
下面,我们在看看 executor 包下的列情况,如下图所示:
- 正如该包下的分包情况,每个包对应一个功能。
statement包,实现向数据库发起 SQL 命令。parameter包,实现设置 PreparedStatement 的占位符参数。- 目前只有一个 ParameterHandler 接口,在 《精尽 MyBatis 源码分析 —— SQL 初始化(下)之 SqlSource》 已经详细解析。
keygen包,实现数据库主键生成( 获得 )的功能。resultset包,实现 ResultSet 结果集的处理,将其映射成对应的结果对象。result包,结果的处理,被resultset包所调用。可能胖友会好奇为啥会有resultset和result两个“重叠”的包。答案见 《精尽 MyBatis 源码分析 —— SQL 执行(四)之 ResultSetHandler》 。loader包,实现延迟加载的功能。- 根目录,Executor 接口及其实现类,作为 SQL 执行的核心入口。
考虑到整个 executor 包的代码量近 5000 行,所以我们将每一个子包,作为一篇文章,逐包解析。所以,本文我们先来分享 根目录,也就是 Executor 接口及其实现类。
2. Executor
org.apache.ibatis.executor.Executor ,执行器接口。代码如下:
// Executor.java
|
- 读和写操作相关的方法
- 事务相关的方法
- 缓存相关的方法
- 设置延迟加载的方法
- 设置包装的 Executor 对象的方法
Executor 的实现类如下图所示:
- 我们可以看到,Executor 的直接子类有 BaseExecutor 和 CachingExecutor 两个。
- 实际上,CachingExecutor 在 BaseExecutor 的基础上,实现二级缓存功能。
- 在下文中,BaseExecutor 的本地缓存,就是一级缓存。
下面,我们按照先看 BaseExecutor 侧的实现类的源码解析,再看 CachingExecutor 的。
3. BaseExecutor
org.apache.ibatis.executor.BaseExecutor ,实现 Executor 接口,提供骨架方法,从而使子类只要实现指定的几个抽象方法即可。
3.1 构造方法
// BaseExecutor.java
|
- 和延迟加载相关,后续文章,详细解析。详细解析,见 《精尽 MyBatis 源码分析 —— SQL 执行(五)之延迟加载》 。
queryStack属性,记录递归嵌套查询的层级。deferredLoads属性,DeferredLoad( 延迟加载 ) 队列。
wrapper属性,在构造方法中,初始化为this,即自己。-
localCache属性,本地缓存,即一级缓存。那什么是一级缓存呢?基于 《MyBatis 的一级缓存实现详解及使用注意事项》 进行修改
每当我们使用 MyBatis 开启一次和数据库的会话,MyBatis 会创建出一个 SqlSession 对象表示一次数据库会话,而每个 SqlSession 都会创建一个 Executor 对象。
在对数据库的一次会话中,我们有可能会反复地执行完全相同的查询语句,如果不采取一些措施的话,每一次查询都会查询一次数据库,而我们在极短的时间内做了完全相同的查询,那么它们的结果极有可能完全相同,由于查询一次数据库的代价很大,这有可能造成很大的资源浪费。
为了解决这一问题,减少资源的浪费,MyBatis 会在表示会话的SqlSession 对象中建立一个简单的缓存,将每次查询到的结果结果缓存起来,当下次查询的时候,如果判断先前有个完全一样的查询,会直接从缓存中直接将结果取出,返回给用户,不需要再进行一次数据库查询了。😈 注意,这个“简单的缓存”就是一级缓存,且默认开启,无法关闭。
如下图所示,MyBatis 会在一次会话的表示 —— 一个 SqlSession 对象中创建一个本地缓存(
localCache),对于每一次查询,都会尝试根据查询的条件去本地缓存中查找是否在缓存中,如果在缓存中,就直接从缓存中取出,然后返回给用户;否则,从数据库读取数据,将查询结果存入缓存并返回给用户。![整体过程]()
- 关于这段话,胖友要理解 SqlSession 和 Executor 和一级缓存的关系。
- 😈 另外,下文,我们还会介绍二级缓存是什么。
-
transaction属性,事务对象。该属性,是通过构造方法传入。为什么呢?待我们看org.apache.ibatis.session.session包。
3.2 clearLocalCache
#clearLocalCache() 方法,清理一级(本地)缓存。代码如下:
// BaseExecutor.java
|
3.3 createCacheKey
#createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) 方法,创建 CacheKey 对象。代码如下:
// BaseExecutor.java
|
<1>处,创建 CacheKey 对象。关于 CacheKey 类,在 《精尽 MyBatis 源码分析 —— 缓存模块》 已经详细解析。<2>处,设置id、offset、limit、sql到 CacheKey 对象中。<3>处,设置 ParameterMapping 数组的元素对应的每个value到 CacheKey 对象中。注意,这块逻辑,和 DefaultParameterHandler 获取value是一致的。<4>处,设置Environment.id到 CacheKey 对象中。
3.4 isCached
#isCached(MappedStatement ms, CacheKey key) 方法,判断一级缓存是否存在。代码如下:
// BaseExecutor.java
|
3.5 query
① #query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) 方法,读操作。代码如下:
// BaseExecutor.java
|
<1>处,调用MappedStatement#getBoundSql(Object parameterObject)方法,获得 BoundSql 对象。<2>处,调用#createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql)方法,创建 CacheKey 对象。<3>处,调用#query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)方法,读操作。通过这样的方式,两个#query(...)方法,实际是统一的。
② #query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) 方法,读操作。代码如下:
// BaseExecutor.java
|
<1>处,已经关闭,则抛出 ExecutorException 异常。<2>处,调用#clearLocalCache()方法,清空本地缓存,如果queryStack为零,并且要求清空本地缓存。例如:<select flushCache="true"> ... </a>。<3>处,queryStack+ 1 。<4.1>处,从一级缓存localCache中,获取查询结果。<4.2>处,获取到,则进行处理。对于#handleLocallyCachedOutputParameters(MappedStatement ms, CacheKey key, Object parameter, BoundSql boundSql)方法,是处理存储过程的情况,所以我们就忽略。<4.3>处,获得不到,则调用#queryFromDatabase()方法,从数据库中查询。详细解析,见 「3.5.1 queryFromDatabase」 。
<5>处,queryStack- 1 。<6.1>处,遍历 DeferredLoad 队列,逐个调用DeferredLoad#load()方法,执行延迟加载。详细解析,见 《精尽 MyBatis 源码分析 —— SQL 执行(五)之延迟加载》 。<6.2>处,清空 DeferredLoad 队列。
-
<7>处,如果缓存级别是LocalCacheScope.STATEMENT,则调用#clearLocalCache()方法,清空本地缓存。默认情况下,缓存级别是LocalCacheScope.SESSION。代码如下:// Configuration.java
/**
* {@link BaseExecutor} 本地缓存范围
*/
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
// LocalCacheScope.java
public enum LocalCacheScope {
/**
* 会话级
*/
SESSION,
/**
* SQL 语句级
*/
STATEMENT
}
3.5.1 queryFromDatabase
#queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) 方法,从数据库中读取操作。代码如下:
// BaseExecutor.java
|
-
<1>处,在缓存中,添加占位对象。此处的占位符,和延迟加载有关,后续可见DeferredLoad#canLoad()方法。// BaseExecutor.java
public enum ExecutionPlaceholder {
/**
* 正在执行中的占位符
*/
EXECUTION_PLACEHOLDER
} -
<2>处,调用#doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)执行读操作。这是个抽象方法,由子类实现。 <3>处,从缓存中,移除占位对象。<4>处,添加结果到缓存中。<5>处,暂时忽略,存储过程相关。
3.5.2 doQuery
// BaseExecutor.java
|
3.6 queryCursor
#queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) 方法,执行查询,返回的结果为 Cursor 游标对象。代码如下:
// BaseExecutor.java
|
<1>处,调用MappedStatement#getBoundSql(Object parameterObject)方法,获得 BoundSql 对象。<2>处,调用#doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)方法,执行读操作。这是个抽象方法,由子类实现。
3.6.1 doQueryCursor
// BaseExecutor.java
|
3.7 update
#update(MappedStatement ms, Object parameter) 方法,执行写操作。代码如下:
// BaseExecutor.java
|
<1>处,已经关闭,则抛出 ExecutorException 异常。<2>处,调用#clearLocalCache()方法,清空本地缓存。因为,更新后,可能缓存会失效。但是,又没很好的办法,判断哪一些失效。所以,最稳妥的做法,就是全部清空。<3>处,调用#doUpdate(MappedStatement ms, Object parameter)方法,执行写操作。这是个抽象方法,由子类实现。
3.7.1 doUpdate
// BaseExecutor.java
|
3.8 flushStatements
#flushStatements() 方法,刷入批处理语句。代码如下:
// BaseExecutor.java
|
isRollBack属性,目前看下来没什么逻辑。唯一看到在 BatchExecutor 中,如果isRollBack = true,则不执行刷入批处理语句。有点奇怪。<1>处,已经关闭,则抛出 ExecutorException 异常。<2>处,调用#doFlushStatements(boolean isRollback)方法,执行刷入批处理语句。这是个抽象方法,由子类实现。
3.8.1 doFlushStatements
// BaseExecutor.java
|
至此,我们已经看到了 BaseExecutor 所定义的四个抽象方法:
- 「3.5.2 doQuery」
- 「3.6.1 doQueryCursor」
- 「3.7.1 doUpdate」
- 「3.8.1 doFlushStatements」
3.9 getTransaction
#getTransaction() 方法,获得事务对象。代码如下:
// BaseExecutor.java
|
3.9.1 commit
// BaseExecutor.java
|
3.9.2 rollback
// BaseExecutor.java
|
3.10 close
#close() 方法,关闭执行器。代码如下:
// BaseExecutor.java
|
3.10.1 isClosed
// BaseExecutor.java
|
3.11 deferLoad
详细解析,见 《精尽 MyBatis 源码分析 —— SQL 执行(五)之延迟加载》 。
3.12 setExecutorWrapper
#setExecutorWrapper(Executor wrapper) 方法,设置包装器。代码如下:
// BaseExecutor.java
|
3.13 其它方法
// BaseExecutor.java
|
- 关于 StatementUtil 类,可以点击
org.apache.ibatis.executor.statement.StatementUtil查看。
4. SimpleExecutor
org.apache.ibatis.executor.SimpleExecutor ,继承 BaseExecutor 抽象类,简单的 Executor 实现类。
- 每次开始读或写操作,都创建对应的 Statement 对象。
- 执行完成后,关闭该 Statement 对象。
4.1 构造方法
// SimpleExecutor.java
|
4.2 doQuery
老艿艿:从此处开始,我们会看到 StatementHandler 相关的调用,胖友可以结合 《精尽 MyBatis 源码分析 —— SQL 执行(二)之 StatementHandler》 一起看。
// SimpleExecutor.java
|
<1>处,调用Configuration#newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)方法,创建 StatementHandler 对象。-
<2>处,调用#prepareStatement(StatementHandler handler, Log statementLog)方法,初始化 StatementHandler 对象。代码如下:// SimpleExecutor.java
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
// <2.1> 获得 Connection 对象
Connection connection = getConnection(statementLog);
// <2.2> 创建 Statement 或 PrepareStatement 对象
stmt = handler.prepare(connection, transaction.getTimeout());
// <2.3> 设置 SQL 上的参数,例如 PrepareStatement 对象上的占位符
handler.parameterize(stmt);
return stmt;
}<2.1>处,调用#getConnection(Log statementLog)方法,获得 Connection 对象。<2.2>处,调用StatementHandler#prepare(Connection connection, Integer transactionTimeout)方法,创建 Statement 或 PrepareStatement 对象。<2.3>处,调用StatementHandler#prepare(Statement statement)方法,设置 SQL 上的参数,例如 PrepareStatement 对象上的占位符。
<3>处,调用StatementHandler#query(Statement statement, ResultHandler resultHandler)方法,进行读操作。<4>处,调用#closeStatement(Statement stmt)方法,关闭 StatementHandler 对象。
4.3 doQueryCursor
// SimpleExecutor.java
|
- 和
#doQuery(...)方法的思路是一致的,胖友自己看下。
4.4 doUpdate
// SimpleExecutor.java
|
- 相比
#doQuery(...)方法,差异点在<3>处,换成了调用StatementHandler#update(Statement statement)方法,进行写操作。
4.5 doFlushStatements
// SimpleExecutor.java
|
- 不存在批量操作的情况,所以直接返回空数组。
5. ReuseExecutor
org.apache.ibatis.executor.ReuseExecutor ,继承 BaseExecutor 抽象类,可重用的 Executor 实现类。
- 每次开始读或写操作,优先从缓存中获取对应的 Statement 对象。如果不存在,才进行创建。
- 执行完成后,不关闭该 Statement 对象。
- 其它的,和 SimpleExecutor 是一致的。
5.1 构造方法
// ReuseExecutor.java
|
5.2 doQuery
// ReuseExecutor.java
|
-
差异一,在于
<1>处,调用#prepareStatement(StatementHandler handler, Log statementLog)方法,初始化 StatementHandler 对象。代码如下:// ReuseExecutor.java
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
// 存在
if (hasStatementFor(sql)) {
// <1.1> 从缓存中获得 Statement 或 PrepareStatement 对象
stmt = getStatement(sql);
// <1.2> 设置事务超时时间
applyTransactionTimeout(stmt);
// 不存在
} else {
// <2.1> 获得 Connection 对象
Connection connection = getConnection(statementLog);
// <2.2> 创建 Statement 或 PrepareStatement 对象
stmt = handler.prepare(connection, transaction.getTimeout());
// <2.3> 添加到缓存中
putStatement(sql, stmt);
}
// <2> 设置 SQL 上的参数,例如 PrepareStatement 对象上的占位符
handler.parameterize(stmt);
return stmt;
}-
调用
#hasStatementFor(String sql)方法,判断是否存在对应的 Statement 对象。代码如下:// ReuseExecutor.java
private boolean hasStatementFor(String sql) {
try {
return statementMap.keySet().contains(sql) && !statementMap.get(sql).getConnection().isClosed();
} catch (SQLException e) {
return false;
}
}- 并且,要求连接未关闭。
-
存在
-
<1.1>处,调用#getStatement(String s)方法,获得 Statement 对象。代码如下:// ReuseExecutor.java
private Statement getStatement(String s) {
return statementMap.get(s);
}- x
- 【差异】
<1.2>处,调用#applyTransactionTimeout(Statement stmt)方法,设置事务超时时间。
-
-
不存在
<2.1>处,获得 Connection 对象。<2.2>处,调用StatementHandler#prepare(Connection connection, Integer transactionTimeout)方法,创建 Statement 或 PrepareStatement 对象。-
【差异】
<2.3>处,调用#putStatement(String sql, Statement stmt)方法,添加 Statement 对象到缓存中。代码如下:// ReuseExecutor.java
private void putStatement(String sql, Statement stmt) {
statementMap.put(sql, stmt);
}- x
<2>处,调用StatementHandler#prepare(Statement statement)方法,设置 SQL 上的参数,例如 PrepareStatement 对象上的占位符。
-
- 差异二,在执行完数据库操作后,不会关闭 Statement 。
5.3 doQueryCursor
// ReuseExecutor.java
|
5.4 doUpdate
// ReuseExecutor.java
|
5.5 doFlushStatements
// ReuseExecutor.java
|
- ReuseExecutor 考虑到重用性,但是 Statement 最终还是需要有地方关闭。答案就在
#doFlushStatements(boolean isRollback)方法中。而 BaseExecutor 在关闭#close()方法中,最终也会调用该方法,从而完成关闭缓存的 Statement 对象们
。 - 另外,BaseExecutor 在提交或者回滚事务方法中,最终也会调用该方法,也能完成关闭缓存的 Statement 对象们。
6. BatchExecutor
org.apache.ibatis.executor.BatchExecutor ,继承 BaseExecutor 抽象类,批量执行的 Executor 实现类。
FROM 祖大俊 《Mybatis3.3.x技术内幕(四):五鼠闹东京之执行器Executor设计原本》
执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理的;BatchExecutor相当于维护了多个桶,每个桶里都装了很多属于自己的SQL,就像苹果蓝里装了很多苹果,番茄蓝里装了很多番茄,最后,再统一倒进仓库。(可以是Statement或PrepareStatement对象)
6.1 构造方法
// BatchExecutor.java
|
currentSql和currentStatement属性,当前 SQL 和 MappedStatement 对象。batchResultList和statementList属性,分别是 BatchResult 和 Statement 数组。并且,每一个batchResultList的 BatchResult 元素,对应一个statementList的 Statement 元素。- 具体怎么应用上述属性,我们见
#doUpdate(...)和#doFlushStatements(...)方法。
6.2 BatchResult
org.apache.ibatis.executor.BatchResult ,相同 SQL 聚合的结果。代码如下:
// BatchResult.java
|
6.3 doUpdate
// BatchExecutor.java
|
<1>处,调用Configuration#newStatementHandler(...)方法,创建 StatementHandler 对象。<2>和<3>处,就是两种不同的情况,差异点在于传入的sql和ms是否匹配当前的currentSql和currentStatement。如果是,则继续聚合到最后的 BatchResult 中,否则,创建新的 BatchResult 对象,进行“聚合”。<2>块:<2.1>、<2.2>、<2.3>处,逻辑和 ReuseExecutor 相似,使用可重用的 Statement 对象,并进行初始化。<2.4>处,获得最后一次的 BatchResult 对象,并添加参数到其中。
<3>块:<3.1>、<3.2>、<3.3>处,逻辑和 SimpleExecutor 相似,创建新的 Statement 对象,并进行初始化。<3.4>处,重新设置currentSql和currentStatement,为当前传入的sql和ms。<3.5>处,添加 Statement 到statementList中。<3.6>处,创建 BatchResult 对象,并添加到batchResultList中。- 那么,如果下一次执行这个方法,如果传递相同的
sql和ms进来,就会聚合到目前新创建的 BatchResult 对象中。
-
<4>处,调用StatementHandler#batch(Statement statement)方法,批处理。代码如下:// 有多个实现类,先看两个
// PreparedStatementHandler.java- 这段代码,是不是非常熟悉。
6.4 doFlushStatements
// BatchExecutor.java
|
<1>处,如果isRollback为true,返回空数组。<2>处,遍历statementList和batchResultList数组,逐个 Statement 提交批处理。<2.1>处,获得 Statement 和 BatchResult 对象。- 【重要】
<2.2>处,调用Statement#executeBatch()方法,批量执行。执行完成后,将结果赋值到BatchResult.updateCounts中。 <2.3>处,处理主键生成。详细解析,见 《精尽 MyBatis 源码分析 —— SQL 执行(三)之 KeyGenerator》 。<2.4>处,调用#closeStatement(stmt)方法,关闭 Statement 对象。<2.5>处,添加到结果集。
<3.1>处,关闭Statement们。<3.2>处,置空currentSql、statementList、batchResultList属性。
6.5 doQuery
// BatchExecutor.java
|
- 和 SimpleExecutor 的该方法,逻辑差不多。差别在于
<1>处,发生查询之前,先调用#flushStatements()方法,刷入批处理语句。
6.6 doQueryCursor
// BatchExecutor.java
|
- 和 SimpleExecutor 的该方法,逻辑差不多。差别在于
<1>处,发生查询之前,先调用#flushStatements()方法,刷入批处理语句。
7. 二级缓存
在开始看具体源码之间,我们先来理解二级缓存的定义:
FROM 凯伦 《聊聊MyBatis缓存机制》
在上文中提到的一级缓存中,其最大的共享范围就是一个 SqlSession 内部,如果多个 SqlSession 之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用 CachingExecutor 装饰 Executor ,进入一级缓存的查询流程前,先在 CachingExecutor 进行二级缓存的查询,具体的工作流程如下所示。
-
那么,二级缓存,反应到具体代码里,是长什么样的呢?我们来打开 MappedStatement 类,代码如下:
// MappedStatement.java
/**
* Cache 对象
*/
private Cache cache;- 就是
cache属性。在前面的文章中,我们已经看到,每个 XML Mapper 或 Mapper 接口的每个 SQL 操作声明,对应一个 MappedStatement 对象。通过@CacheNamespace或<cache />来声明,创建其所使用的 Cache 对象;也可以通过@CacheNamespaceRef或<cache-ref />来声明,使用指定 Namespace 的 Cache 对象。 -
最终在 Configuration 类中的体现,代码如下:
// Configuration.java
/**
* Cache 对象集合
*
* KEY:命名空间 namespace
*/
protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");- 一个 KEY 为 Namespace 的 Map 对象。
- 可能上述描述比较绕口,胖友好好理解下。
- 就是
-
通过在
mybatis-config.xml中,配置如下开启二级缓存功能:<setting name="cacheEnabled" value="true"/>
7.1 CachingExecutor
org.apache.ibatis.executor.CachingExecutor ,实现 Executor 接口,支持二级缓存的 Executor 的实现类。
7.1.1 构造方法
// CachingExecutor.java
|
tcm属性,TransactionalCacheManager 对象,支持事务的缓存管理器。因为二级缓存是支持跨 Session 进行共享,此处需要考虑事务,那么,必然需要做到事务提交时,才将当前事务中查询时产生的缓存,同步到二级缓存中。这个功能,就通过 TransactionalCacheManager 来实现。<1>处,设置delegate属性,为被委托的 Executor 对象。<2>处,调用delegate属性的#setExecutorWrapper(Executor executor)方法,设置delegate被当前执行器所包装。
7.1.2 直接调用委托方法
CachingExecutor 的如下方法,具体的实现代码,是直接调用委托执行器 delegate 的对应的方法。代码如下:
// CachingExecutor.java
|
7.1.3 query
// CachingExecutor.java
|
<1>处,调用MappedStatement#getCache()方法,获得 Cache 对象,即当前 MappedStatement 对象的二级缓存。<3>处,如果没有 Cache 对象,说明该 MappedStatement 对象,未设置二级缓存,则调用delegate属性的#query(...)方法,直接从数据库中查询。-
<2>处,如果有 Cache 对象,说明该 MappedStatement 对象,有设置二级缓存:-
<2.1>处,调用#flushCacheIfRequired(MappedStatement ms)方法,如果需要清空缓存,则进行清空。代码如下:// CachingExecutor.java
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) { // 是否需要清空缓存
tcm.clear(cache);
}
}- 通过
@Options(flushCache = Options.FlushCachePolicy.TRUE)或<select flushCache="true">方式,开启需要清空缓存。 - 调用
TransactionalCache#clear()方法,清空缓存。注意,此时清空的仅仅,当前事务中查询数据产生的缓存。而真正的清空,在事务的提交时。这是为什么呢?还是因为二级缓存是跨 Session 共享缓存,在事务尚未结束时,不能对二级缓存做任何修改。😈 可能有点绕,胖友好好理解。
- 通过
<2.2>处,当MappedStatement#isUseCache()方法,返回true时,才使用二级缓存。默认开启。可通过@Options(useCache = false)或<select useCache="false">方法,关闭。<2.3>处,调用TransactionalCacheManager#getObject(Cache cache, CacheKey key)方法,从二级缓存中,获取结果。- 如果不存在缓存
<2.4.1>处,调用delegate属性的#query(...)方法,再从数据库中查询。<2.4.2>处,调用TransactionalCacheManager#put(Cache cache, CacheKey key, Object value)方法,缓存结果到二级缓存中。😈 当然,正如上文所言,实际上,此处结果还没添加到二级缓存中。那具体是怎么样的呢?答案见 TransactionalCache 。
- 如果存在缓存
<2.5>处,如果存在,则直接返回结果。
-
7.1.4 queryCursor
// CachingExecutor.java
|
- 无法开启二级缓存,所以只好调用
delegate对应的方法。
7.1.5 update
// CachingExecutor.java
|
7.1.6 commit
// CachingExecutor.java
|
delegate和tcm先后提交。
7.1.7 rollback
// CachingExecutor.java
|
delegate和tcm先后回滚。
7.1.8 close
// CachingExecutor.java
|
- 根据
forceRollback属性,进行tcm和delegate对应的操作。
7.2 TransactionalCacheManager
org.apache.ibatis.cache.TransactionalCacheManager ,TransactionalCache 管理器。
7.2.1 构造方法
// TransactionalCacheManager.java
|
- 我们可以看到,
transactionalCaches是一个使用 Cache 作为 KEY ,TransactionalCache 作为 VALUE 的 Map 对象。 - 为什么是一个 Map 对象呢?因为在一次的事务过程中,可能有多个不同的 MappedStatement 操作,而它们可能对应多个 Cache 对象。
-
TransactionalCache 是怎么创建的呢?答案在
#getTransactionalCache(Cache cache)方法,代码如下:// TransactionalCacheManager.java
private TransactionalCache getTransactionalCache(Cache cache) {
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}- 优先,从
transactionalCaches获得 Cache 对象,对应的 TransactionalCache 对象。 - 如果不存在,则创建一个 TransactionalCache 对象,并添加到
transactionalCaches中。
- 优先,从
7.2.2 putObject
#putObject(Cache cache, CacheKey key, Object value) 方法,添加 Cache + KV ,到缓存中。代码如下:
// TransactionalCacheManager.java
|
7.2.3 getObject
#getObject(Cache cache, CacheKey key) 方法,获得缓存中,指定 Cache + K 的值。代码如下:
// TransactionalCacheManager.java
|
7.2.4 clear
#clear() 方法,清空缓存。代码如下:
// TransactionalCacheManager.java
|
7.2.5 commit
#commit() 方法,提交所有 TransactionalCache 。代码如下:
// TransactionalCacheManager.java
|
- 通过调用该方法,TransactionalCache 存储的当前事务的缓存,会同步到其对应的 Cache 对象。
7.2.6 rollback
#rollback() 方法,回滚所有 TransactionalCache 。代码如下:
// TransactionalCacheManager.java
|
7.3 TransactionalCache
org.apache.ibatis.cache.decorators.TransactionalCache ,实现 Cache 接口,支持事务的 Cache 实现类,主要用于二级缓存中。英语比较好的胖友,可以看看如下注释:
This class holds all cache entries that are to be added to the 2nd level cache during a Session.
Entries are sent to the cache when commit is called or discarded if the Session is rolled back.
Blocking cache support has been added. Therefore any get() that returns a cache miss
will be followed by a put() so any lock associated with the key can be released.
7.3.1 构造方法
// TransactionalCache.java
|
- 胖友认真看下每个变量上的注释。
- 在事务未提交时,
entriesToAddOnCommit属性,会暂存当前事务新产生的缓存 KV 对。 - 在事务提交时,
entriesToAddOnCommit属性,会同步到二级缓存delegate中。
7.3.2 getObject
// TransactionalCache.java
|
<1>处,调用delegate的#getObject(Object key)方法,从delegate中获取key对应的 value 。<2>处,如果不存在,则添加到entriesMissedInCache中。这是个神奇的逻辑???答案见commit()和#rollback()方法。<3>处,如果clearOnCommit为true,表示处于持续清空状态,则返回null。因为在事务未结束前,我们执行的清空缓存操作不好同步到delegate中,所以只好通过clearOnCommit来标记处于清空状态。那么,如果处于该状态,自然就不能返回delegate中查找的结果。<4>处,返回 value 。
7.3.3 putObject
#putObject(Object key, Object object) 方法,暂存 KV 到 entriesToAddOnCommit 中。代码如下:
// TransactionalCache.java
|
7.3.4 removeObject
// TransactionalCache.java
|
- 不太明白为什么是这样的实现。不过目前也暂时不存在调用该方法的情况。暂时忽略。
7.3.5 clear
#clear() 方法,清空缓存。代码如下:
// TransactionalCache.java
|
<1>处,标记clearOnCommit为true。<2>处,清空entriesToAddOnCommit。- 该方法,不会清空
delegate的缓存。真正的清空,在事务提交时。
7.3.6 commit
#commit() 方法,提交事务。重头戏,代码如下:
// TransactionalCache.java
|
<1>处,如果clearOnCommit为true,则清空delegate缓存。-
<2>处,调用#flushPendingEntries()方法,将entriesToAddOnCommit、entriesMissedInCache同步到delegate中。代码如下:// TransactionalCache.java
private void flushPendingEntries() {
// 将 entriesToAddOnCommit 刷入 delegate 中
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
// 将 entriesMissedInCache 刷入 delegate 中
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}- 在看这段代码时,笔者一直疑惑
entriesMissedInCache同步到delegate中,会不会存在问题。因为当前事务未查找到,不代表其他事务恰好实际能查到。这样,岂不是会将缓存错误的置空。后来一想,缓存即使真的被错误的置空,最多也就多从数据库中查询一次罢了。😈
- 在看这段代码时,笔者一直疑惑
-
<3>处,调用#reset()方法,重置对象。代码如下:// TransactionalCache.java
private void reset() {
// 重置 clearOnCommit 为 false
clearOnCommit = false;
// 清空 entriesToAddOnCommit、entriesMissedInCache
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}- 因为,一个 Executor 可以提交多次事务,而 TransactionalCache 需要被重用,那么就需要重置回初始状态。
7.3.7 rollback
#rollback() 方法,回滚事务。代码如下:
// TransactionalCache.java
|
-
<1>处,调用#unlockMissedEntries()方法,将entriesMissedInCache同步到delegate中。代码如下:// TransactionalCache.java
private void unlockMissedEntries() {
for (Object entry : entriesMissedInCache) {
try {
delegate.removeObject(entry);
} catch (Exception e) {
log.warn("Unexpected exception while notifiying a rollback to the cache adapter."
+ "Consider upgrading your cache adapter to the latest version. Cause: " + e);
}
}
}- 即使事务回滚,也不妨碍在事务的执行过程中,发现
entriesMissedInCache不存在对应的缓存。
- 即使事务回滚,也不妨碍在事务的执行过程中,发现
<2>处,调用#reset()方法,重置对象。
8. 创建 Executor 对象
在上面的文章中,我们已经看了各种 Executor 的实现代码。那么,Executor 对象究竟在 MyBatis 中,是如何被创建的呢?Configuration 类中,提供 #newExecutor(Transaction transaction, ExecutorType executorType) 方法,代码如下:
// Configuration.java
|
-
<1>处,获得执行器类型。可以通过在mybatis-config.xml配置文件,如下:// value 有三种类型:SIMPLE REUSE BATCH
<setting name="defaultExecutorType" value="" /> -
org.apache.ibatis.session.ExecutorType,执行器类型。代码如下:// ExecutorType.java
public enum ExecutorType {
/**
* {@link org.apache.ibatis.executor.SimpleExecutor}
*/
SIMPLE,
/**
* {@link org.apache.ibatis.executor.ReuseExecutor}
*/
REUSE,
/**
* {@link org.apache.ibatis.executor.BatchExecutor}
*/
BATCH
} -
<2>处,创建对应实现的 Executor 对象。默认为 SimpleExecutor 对象。 <3>处,如果开启缓存,创建 CachingExecutor 对象,进行包装。<4>处,应用插件。关于插件,我们在后续的文章中,详细解析。
9. ClosedExecutor
老艿艿:写到凌晨 1 点多,以为已经写完了,结果发现….
在 ResultLoaderMap 类中,有一个 ClosedExecutor 内部静态类,继承 BaseExecutor 抽象类,已经关闭的 Executor 实现类。代码如下:
// ResultLoaderMap.java
|
- 仅仅在 ResultLoaderMap 中,作为一个“空”的 Executor 对象。没有什么特殊的意义和用途。
10. ErrorContext
org.apache.ibatis.executor.ErrorContext ,错误上下文,负责记录错误日志。代码比较简单,也蛮有意思,胖友可以自己研究研究。




浙公网安备 33010602011771号