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
,错误上下文,负责记录错误日志。代码比较简单,也蛮有意思,胖友可以自己研究研究。