一、MyBatis 执行器

1.1、JDBC 执行过程

1、执行流程

JDBC 的执行流程大致如下图

image-20210710140614956

  • 获取连接(Connection)

基于事务来获取连接

1
2
3
/** 第一步: 获取连接 */
Connection connection = DriverManager
.getConnection(JDBC.URL, JDBC.USERNAME, JDBC.PASSWORD);

image-20210710141144502

  • 构建 Statement 对象

通过连接 Connection 对象来构造 Statement 对象

image-20210710141251476

  • 设置参数

image-20210710141314253

  • 执行修改

image-20210710141345565

2、JDBC 中的 Statement

JDBC 有三种执行器,分别是

  • Statement(简单执行器)

执行静态SQL

  • PreparedStatement(预处理执行器)

设置预编译,防止SQL注入

  • CallableStatement(存储过程执行器)

设置出参、读取参数(用于执行存储过程)

  • 三者继承关系

CallableStatement 继承自 PreparedStatement ,而 PreparedStatement 又向上继承 Statement

1.2、MyBatis 核心组件(SqlSession 与 Executor)

image-20210710141948414

MyBatis 在执行时关系到四个重要模块,分别是 动态代理(MapperProxy)SQL 会话(SqlSession)执行器(Executor)JDBC 处理器(StatementHandler)

1、MyBatis 执行过程 – SQL 会话(SqlSession)

MyBatis 的 SQL 会话(SqlSession)采用门面模式设计,其核心作用是为用户提供一个统一的门面接口 API ,使得系统更容易使用

SqlSession 中的 API 包括增、删、改、查(基本 API )以及提交、关闭(辅助 API )等。其自身是没有能力处理这些请求的,所以内部会包含一个唯一的执行器 Executor,所有请求都会交给执行器来处理。

如下图中SqlSession接收用户“修改”请求,然后转交给Executor

image-20210710142131413

SqlSession 如何将请求交给执行器?SqlSession 内部会存在一个 executor 属性,这个属性指向真实的执行器对象,当我们执行 CRUD 时,对应的方法会转交给实际的执行器对象

image-20210710142202596

执行器实际上只有改查两个基本功能,为什么?

这是因为 JDBC 提供的 标准API 中只有 executeUpdate(改)与 executeQuery(查)两个功能。

image-20210709162116151

2、MyBatis 执行过程 – 执行器(Executor)

Executor是一个大管家,核心功能包括:缓存维护获取动态SQL获取连接、以及最终的JDBC调用等。在图中所有蓝色节点全部都是在Executor中完成。

Executor 的出现是为了处理某些共性功能,如缓存、获取连接等

这么多事情无法全部亲力亲为,就需要把任务分派下去。所以Executor内部还会包含若干个组件:

  1. 缓存维护:cache
  2. 获取连接:Transaction
  3. 获取动态sql:SqlSource
  4. 调用jdbc:StatementHandler

上述组件中前三个和Executor是1对1关系,只有StatementHandler是1对多。每执行一次SQL 就会构造一个新的StatementHandler。

3、执行器实现 – 简单执行器 SimpleExecutor

简单执行器基本实现了执行器的所有 Api ,无论执行的 SQL 是否一样,简单执行器在每次执行之前都会进行一次预处理,效率不高。

每次都会创建一个新的预处理器,即 PrepareStatement 对象

4、执行器实现 – 可重用执行器 ReuseExecutor

对于相同的 SQL 语句,ReuseExecutor 只会预编译一次。

对于相同的 SQL ,不会进行重复的预处理

5、执行器实现 – 批处理执行器 BatchExecutor

批处理只针对增删改,即对于一次提交的多次增删改操作,只会进行一次预处理。

在使用 BatchExecutor 时,需要进行手动提交,将批处理提交的数据一次刷新到数据库中。

6、执行器抽象类 – BaseExecutor

image-20210710150330500

基础执行器是上面三个执行器的父类,实现了三个执行器实现类的一些重复操作,包括一级缓存获取连接

image-20210710150045376

在 BaseExecutor 中存在着两个抽象方法,分别是执行增删改的 doUpdate 方法和执行查询的 doQuery 方法,它的三个实现子类在各自的类中分别实现这三个方法,以此达到执行不同的操作

  • BaseExecutor 中的 doUpdate 抽象方法

实现类在自己的 doUpdate 方法中编写清理缓存的逻辑。

1
2
3
protected abstract int doUpdate(MappedStatement ms, 
Object parameter)
throws SQLException;
  • BaseExecutor 中的 doQuery 抽象方法

实现类在自己的 doQuery 方法中编写使用缓存的逻辑。

1
2
3
4
5
6
protected abstract <E> List<E> doQuery(MappedStatement ms, 
Object parameter,
RowBounds rowBounds,
ResultHandler resultHandler,
BoundSql boundSql)
throws SQLException;
  • BaseExecutor 中的 query 方法
1
2
3
4
5
6
7
8
9
10
11
12
 @Override
public <E> List<E> query(MappedStatement ms,
Object parameter,
RowBounds rowBounds,
ResultHandler resultHandler) throws SQLException {
// 获取动态 SQL
BoundSql boundSql = ms.getBoundSql(parameter);
// 创建缓存的 key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 调用重载的 query 方法
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
  • 重载的 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
@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());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
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--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
  • queryFromDatabase 方法

可以看到,在 queryFromDatabase 方法中调用了其子类实现的 doQuery 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}

7、MyBatis 实现二级缓存 – CachingExecutor

MyBatis 没有在 BaseExecutor 中加入与二级缓存相关的功能,因为二级缓存是需要手动开启的;对于二级缓存,MyBatis 使用另一个 CachingExecutor 类来实现 Executor 接口,这个类不是一个抽象类

CachingExecutor 只专注于实现二级缓存,它实际上没有与数据库进行交互的功能,但它类中声明了一个 Executor 的属性 delegate ,这个属性指向一个 BaseExecutor 对象,delegate 属性就是被装饰的对象。

所以说,CachingExecutor 对象中与数据库进行交互的功能是由 BaseExecutor 对象提供的。

这是 装饰者模式 的具体实现,在不改变原有类结构和继承的情况下,通过包装源对象去扩展一个新功能。

相当于为 BaseExecutor 装饰了一层二级缓存的功能,保证了类的单一职责。

先走二级缓存,后走一级缓存

image-20210710152457486

  • CachingExecutor 中的 update 方法

image-20210710152924276

  • CachingExecutor 中的 query 方法

2021-07-10_152957 (2)

在开启二级缓存的条件下,执行查询操作时的操作步骤如下

  1. 调用 SqlSession 的 API ,SqlSession 将请求及参数交给其类中的 Executor 属性执行,注意,此时这个 Executor 指向的应该是一个 CachingExecutor 对象

  2. 先查询二级缓存,如果没有二级缓存,那么调用 CachingExecutor 中的 deltegate 属性访问数据库,如果有,直接返回二级缓存,这里的 delegate 是一个 BaseExecutor 对象,其会调用实现子类的方法访问一级缓存或者与数据库进行交互

二、MyBatis 的缓存

MyBatis 的缓存分为一级缓存和二级缓存,其中二级缓存在 CachingExecutor 中实现,一级缓存在 BaseExecutor 中实现

2.1、一级缓存

1、说明

一级缓存也称为会话级缓存,指的是在同一会话内如果有两次相同的查询(Sql和参数均相同),那么第二次就会命中缓存。一级缓存通过会话进行存储,当会话关闭,缓存也就没有了。此外如果会话进行了修改(增删改) 操作,缓存也会被清空。

注意,如果执行的 SQL 和参数均相同,但 Statement Id 不同,那么一级缓存也不会生效,这里的 Statement Id 为 (Mapper 接口全类名 + 方法)。

一级缓存默认是开启的,而且不能关闭

  • 为什么一级缓存无法关闭?

因为 MyBatis 的一些关键特性,如通过 建立级联映射、避免循环引用(circular references)、加速重复嵌套查询等 都是基于 MyBatis 的一级缓存实现的,而且 MyBatis 结果集映射相关代码重度依赖 CacheKey,所以目前MyBatis不支持关闭一级缓存。

2、MyBatis 中一级缓存的级别

MyBatis 提供了一个配置参数 localCacheScope,用于控制一级缓存的级别,该参数的取值为 SESSION、STATEMENT

  • 当指定 localCacheScope 参数值为 SESSION 时,缓存对整个 SqlSession 有效,只有执行 DML 语句(更新语句)时,缓存才会被清除。
  • 当 localCacheScope 值为 STATEMENT 时,缓存仅对当前执行的语句有效,当语句执行完毕后,缓存就会被清空。

3、源码解析

一级缓存底层使用一个 HashMap 实现,存储结构为 key-value 结构

image-20210710172443663

在上图中,如果不存在一级缓存,那么会执行 BaseExecutor 子类的 doQuery 方法访问数据库,在从数据库查询到数据后,将数据填充到一级缓存中,如下图所示

image-20210710172736928

  • 查看 BaseExecutor 中的 query 方法源码
1
2
3
4
5
6
7
8
9
10
11
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
...
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
...
}
return list;
}

从上面的源码可以看到,在 query 方法中使用 localCache 属性的 getObject 方法获取缓存,这个方法需要传入一个 CacheKey 对象,这个 localCache 在类中的声明如下

image-20210710173313187

可以看到 localCache 是一个 PerpetualCache 类型的属性,我们下一步来查看 PerpetualCache 的源码

  • PerpetualCache 的源码

image-20210710173557417

同时,PerpetualCache 类中的 getObject 方法底层调用了 HashMap 的 get 方法

image-20210710173732639

4、CacheKey

MyBatis 通过 CacheKey 对象来描述缓存的 Key 值。
在进行查询操作时,首先创建 CacheKey 对象( CacheKey 对象决定了缓存的 Key 与哪些因素有关系)。
如果两次查询操作 CacheKey 对象相同,就认为这两次查询执行的是相同的SQL语句。

CacheKey 对象通过 BaseExecutor 类的 createCacheKey() 方法创建,代码如下:

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
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
// 获取 StatementId
cacheKey.update(ms.getId());
// 获取分页信息 -- 偏移量
cacheKey.update(rowBounds.getOffset());
// 获取分页信息 -- 查询记录数
cacheKey.update(rowBounds.getLimit());
// 获取执行的 SQL
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// 获取参数
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());
}
// 返回 cacheKey 对象
return cacheKey;
}

CacheKey 类中的结构如下

  • multiplier

计算hashcode的乘积数,默认为 37

  • hashCode

默认为17

  • updateList

用于决定是否命中缓存的变量,这是一个 Object 对象列表,里面通常填充六个值

  1. Statement Id : 区分 Mapper 接口方法的唯一指定值,为 Mapper 接口 + 方法名组成
  2. offset : 分页偏移量,默认为 0
  3. limit : 查询数据条数,默认为 Integer.MAX_VALUE,这个属性与上面的属性构成分页条件
  4. SQL :查询的 SQL 语句
  5. parameterValue : 查询参数的值
  6. environmentId : 环境变量,在配置mybatis环境的时候有一个标签(environment) ,他有一个属性id;environmentId的就是这个id的值;

在多次查询中,只有 updateList 中的六个变量全部吻合,那么才能命中一级缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CacheKey implements Cloneable, Serializable {

private static final long serialVersionUID = 1146682552656046210L;

public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();

private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;

private final int multiplier;
private int hashcode;
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.
private List<Object> updateList;

public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLYER;
this.count = 0;
this.updateList = new ArrayList<>();
}
}

断点追踪

image-20210710175408692

5、写入一级缓存

在 BaseExecutor 的 queryFromDataBase 方法中与数据库进行交互,在从数据库中查询到数据后,会调用 localCache 的 putObject 方法将查询到的数据放入数据库中。

image-20210710180304002

6、清空一级缓存

MyBatis 使用 clearLocalCache 方法来清空一级缓存

image-20210710180723202

  • 执行 update 时会调用 clearLocalCache 方法清空一级缓存

BaseExecutor 类中的 update 方法源码如下

image-20210710180910115

  • 在查询之前如果配置 flushCache = true ,那么也会清空缓存

image-20210710181117425

  • 如果缓存的作用域是 STATEMENT ,那么每次查询前也会清空一级缓存

注意,清空缓存不能发生在子查询中。

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
...
if (queryStack == 0) {
...
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
  • 执行 rollback 和 commit 都会清空缓存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 @Override
public void commit(boolean required) throws SQLException {
...
clearLocalCache();
flushStatements();
if (required) {
transaction.commit();
}
}
@Override
public void rollback(boolean required) throws SQLException {
if (!closed) {
try {
clearLocalCache();
flushStatements(true);
} finally {
if (required) {
transaction.rollback();
}
}
}
}

7、一级缓存失效

  • Spring 整合 MyBatis 后,可能会出现一级缓存失效的情况。

这是由于在未开启事务的情况下,每次查询,Spring 都会关闭旧的 SqlSession 对象然后创建一个新的 SqlSession 对象,由于一级缓存是会话级缓存,所以不同会话的查询之间自然无法使用一级缓存。

开启事务后,Spring 会使用 ThreadLocal 获取当前资源绑定同一个 SqlSession 对象,因为一级缓存是有效的。

  • 解决方法

添加事务即可。

2.2、二级缓存

1、说明

二级缓存也称为应用级缓存,与一级缓存不同,它的作用范围是整个应用,并且可以跨线程使用。

所以二级缓存拥有更高的命中率,适合缓存一些修改较少的数据。

由于生命周期长,跨会话访问的因素所以二级在使用上要更谨慎,如果用的不好就会造成脏读

2、设置方法

  • 需要在 MyBatis 的主配置文件中的 settings 标签中设置 cacheEnable = true=

  • 然后在 Mapper 文件中配置缓存策略、刷新频率和缓存容量等信息。

3、二级缓存扩展性需求

  • 存储

一级缓存底层使用 HashMap 来实现缓存,我们可以将二级缓存中的数据保存在内存(速度快但断电即失)、硬盘或者是第三方缓存中(Redis

二级缓存底层的存储格式还是 key:value

  • 缓存淘汰策略

由于二级缓存的生命周期很长,所以需要使用淘汰策略在空间不足时对一些缓存进行淘汰,常见的有 FIFO ,即先进先出的淘汰策略,除此之外,还有 LRU ,即淘汰最近最少使用的缓存。

  • 过期清理

  • 线程安全

  • 命中率统计

  • 序列化

3、责任链设计模式

责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。

拦截的类需要实现统一接口,由于在实现缓存中需要设计大量操作,那么可以使用多个拥有不同职责的类来接收请求,这些类实现了统一接口并使用装饰者模式进行依赖,当一个类对象处理完请求的部分功能(如序列化、缓存命中计算)后,再将自己处理不了的需求通过 delegate 属性传递下去,交给其他类进行处理。

4、MyBatis 实现二级缓存

MyBatis 提供了 Cache 接口来实现二级缓存,当我们需要自定义二级缓存时,可以通过实现 Cache 接口来实现。

查看 Cache 接口

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
public interface Cache {

/**
* 用于返回此缓存的标识符
*/
String getId();

/**
* 用于设置缓存
*/
void putObject(Object key, Object value);

/**
* 通过key获取缓存
*/
Object getObject(Object key);

/**
* 通过key移除缓存
*/
Object removeObject(Object key);

/**
* 清除缓存
*/
void clear();

/**
* 获取缓存大小
*/
int getSize();

/**
* 获取读写锁
*/
ReadWriteLock getReadWriteLock();
}

实现二级缓存的过程中, MyBatis 使用了装饰器 + 责任链模式,提升了程序的扩展性和逻辑性

image-20210710210426406

追踪源码可以看到,SynchronizedCache 中存在着一个 delegate 属性,这个属性是一个 LoggingCache 对象,同时 LoggingCache 中又有一个 SerializedCache 类型的属性 delegate…

每个 Cache 对象处理完自己的工作后,将处理不了的功能传递到下一个对象中。

使用了责任链 + 装饰器模式

image-20210710212027494

  • 不同的功能由不同的缓存装饰器实现,下标是装饰器类与对应功能
装饰器描述
SynchronizedCache同步锁,用于保证对指定缓存区的操作都是同步的
LoggingCache统计器,记录缓存命中率
BlockingCache阻塞器,基于key加锁,防止缓存穿透
ScheduledCache时效检查,用于验证缓存有效器,并清除无效数据
LruCache溢出算法,淘汰闲置最久的缓存。
FifoCache溢出算法,淘汰加入时间最久的缓存
WeakCache溢出算法,基于java弱引用规则淘汰缓存
SoftCache溢出算法,基于java软引用规则淘汰缓存
PerpetualCache实际存储,内部采用HashMap进行存储。

5、缓存命中条件

MyBatis一二级缓存的CacheKey是一致的,必须满足以条件才可以命中缓存

  1. 相同的statement id
  2. 相同的Sql与参数
  3. 返回行范围相同
  4. 没有使用ResultHandler来自定义返回数据
  5. 没有配置UseCache=false 来关闭缓存
  6. 没有配置FlushCache=true 来清空缓存
  7. 在调用存储过程中不能使用出参,即Parameter中mode=out|inout

6、二级缓存写入

与一级缓存的实时写入不同,二级缓存是在事务提交或会话关闭之后才会触发缓存写入

这么做其实也好理解,因为二级缓存是跨会话的,如果没有提交就写入,如果事务最后回滚,肯定导致别的会话脏读。

image-20210710221301358

每一个会话都有自己对应的一块事务缓存管理器,管理器中拥有许许多多暂存区,在查询后,会先将查询到的数据放入暂存区中,如果事务提交,那么将暂存区中的数据放入对应的缓存区中,否则就不放入缓存区中。

暂存区 (TransactionalCache)用于暂时存放待缓存的数据区域,和缓存区是一一对应的。如果会话会涉及多个二级缓存的访问,那么对应暂存区也会有多个。暂存区生命周期与会话保持一致。

image-20210710221550016

7、二级缓存的存取流程

  • 我们先看一下 MyBatis 中 Executor 的体系结构图

image-20210711135816143

可以看到, CachingExecutor 在 BaseExecutor 之前,故 MyBatis 是先访问二级缓存,后访问一级缓存。

  • 二级缓存的存取过程如下图

image-20210711141426196

  • 查询

先判断是否存在二级缓存,如果存在二级缓存,那么从二级缓存中查询,如果不存在二级缓存,那么调用 BaseExecutor 及其子类查询数据库,同时将查询到的数据填充到对应的暂存区中

  • 修改

在执行修改操作时,会默认调用 flushCacheIfRequired 方法清空缓存,注意,这里清空的只是暂存区,而不是真正的清空二级缓存

真正的清空发生在事务提交后,未提交前只是令二级缓存失效,MyBatis 提供了一个 clearOnCommit 标记,当这个值为 true 时,标记二级缓存是否失效,当这个值为 true 时,即使二级缓存中有值,那么也只会返回 null

下面是二级缓存空间类 TransactionalCache 中的 getObject 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public Object getObject(Object key) {
// issue #116
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) {
// 如果clearOnCommit为true,直接返回 null
return null;
} else {
return object;
}
}
  • 提交

在提交事务时,会将暂存区中的缓存保存到二级缓存中

8、源码分析

  • CachingExecutor 中的 query 方法
  1. 事务管理器的 getObject 方法需要传入缓存空间和 key
  2. 如果从缓存中获取到的对象为空,那么调用 BaseExecutor 的 query 方法查询数据库,并将查询到的数据放入暂存区中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
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);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 从事务管理器中获取缓存,这里需要传入缓存空间 Cache 和 缓存对应的 key
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;
}
}
// 如果cache为空,那么直接查询数据库
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
  • 查看事务管理器中的源码

image-20210711143328276

可以看到事务管理器中维护了一个 Map ,这个 Map 以 Cache 对象,即暂存区对象为键,二级缓存空间为值

这样可以通过暂存区唯一找到一块二级缓存空间

image-20210711143510428

  • 这就是前面所讲的,每个暂存区对应二级缓存空间中的一块指定空间,在进行查询时,我们需要使用暂存区获取二级缓存中的指定空间

image-20210711143820708

三、MyBatis 执行流程回顾

当查询缓存时,那么不会与 StatementHandler 打交道

StatementHandler 用于与 JDBC 进行交互,如声明 Statement 与填充参数等

image-20210711150954577

笔记参考来源如下: