0%

Mybatis

Mybatis备忘

缓存

默认缓存设置

在MyBatis中,关于缓存设置的参数一共有2个:localCacheScope,cacheEnabled。

1
2
3
4
<!-- 有效值: true|false,默认值为true -->
<setting name="cacheEnabled" value="true" />
<!-- 有效值:SESSION|STATEMENT,默认值为SESSION -->
<setting name="localCacheScope" value="SESSION" />

那么这两个参数分别在什么地方使用呢?不妨先走读一下MyBatis的相关源码。

首先,来看看参数cacheEnabled的应用。

  • org.apache.ibatis.session.defaults.DefaultSqlSessionFactory
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
// 在应用程序中通过sqlSessionFactory获取一个SqlSession对象执行CRUD操作
SqlSession sqlSession = sqlSessionFactory.openSession(true);

// 在DefaultSqlSessionFactory中获取SqlSession对象
@Override
public SqlSession openSession(boolean autoCommit) {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, autoCommit);
}

// 通过MyBatis配置参数构建SqlSession对象
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 根据配置的Executor类型装配具体的实现类
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
  • org.apache.ibatis.session.Configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 在Configuration中根据不同的defaultExecutorType参数值装配具体的Executor实现
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
// 根据不同的defaultExecutorType参数值装配具体的Executor实现
Executor executor;
if (ExecutorType.BATCH == executorType) {
// 当defaultExecutorType值为BATCH时,使用BatchExecutor
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
// 当defaultExecutorType值为REUSE时,使用ReuseExecutor
executor = new ReuseExecutor(this, transaction);
} else {
// 默认情况下使用SimpleExecutor
executor = new SimpleExecutor(this, transaction);
}
// 如果设置cacheEnabled参数值为true,将使用CachingExecutor
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}

从上述源码中可以看到,MyBatis会根据配置参数defaultExecutorType的值使用不同的执行器:BatchExecutor,ReuseExecutor,SimpleExecutor。此外,当参数cacheEnabled值为true时,会使用一个特别的执行器:CachingExecutor。那么,不同的执行器有什么不同呢?他们有什么联系吗?下图为MyBatis中执行器的类图。

OK,到这里我们就可以对MyBatis中控制缓存的2个参数做一个浅显的总结:
(1)参数cacheEnabled控制MyBatis使用的执行器类型
(2)参数localCacheScope控制的是BaseExecutor内部的缓存策略

缓存实现原理分析

参数localCacheScope控制的缓存策略

在相同的SqlSession中查询同一条数据时都会命中BaseExecutor的本地缓存。也就是说通过参数localCacheScope控制的缓存策略只能在相同SqlSession内有效,因为BaseExecutor的本地缓存对象localCache是实例属性,在不同的执行器实例中都保存一个独立的本地缓存,而在不同的SqlSession中使用的是不同的执行器实例。这个关系可以通过下图描述:

那么,到底是不是这样的呢?我们需要进行验证。

1
2
3
<!-- 为了验证BaseExecutor内的缓存策略,需要设置cacheEnabled参数为false,默认值为true -->
<setting name="cacheEnabled" value="false"/>
<setting name="localCacheScope" value="SESSION"/>
1
2
3
4
5
6
7
// 在相同Session中查询同一条数据
SqlSession sqlSession = sqlSessionFactory.openSession(true);
Student student = sqlSession.getMapper(StudentMapper.class).getStudentById(1);
System.out.println(student);
student = sqlSession.getMapper(StudentMapper.class).getStudentById(1);
System.out.println(student);
sqlSession.close();

对应MyBatis输出日志如下:

1
2
3
4
5
6
7
8
9
10
11
method: query
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 1131184547.
DEBUG [main] - ==> Preparing: select * from student where id = ?
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <== Total: 1
Student{id=1, name='张三', age=23, sex=0}
method: query
Student{id=1, name='张三', age=23, sex=0}
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@436c81a3]
DEBUG [main] - Returned connection 1131184547 to pool.

显然,从输出日志中可以很确定地知道:在相同Session中查询同一条数据时,只有第一次会真正从数据库中查询,后续的查询都会直接从Session内的缓存中获取。而且,我们从上述相关源码中知道,只要SqlSession存在,该缓存是永远存在,不会失效。

参数cacheEnabled控制的缓存策略

在了解了参数localCacheScope控制的缓存策略之后,还需要继续研究参数cacheEnabled所控制的缓存策略。从上述源码分析中已经知道,当参数cacheEnabled值为true时,MyBatis将使用CachingExecutor执行器,下面通过源码解读一下CachingExecutor到底与其他Executor实现类有什么不同。

  • org.apache.ibatis.executor.CachingExecutor

在不同的Session中查询同一条数据时都会从这个全局缓存中查询,下面通过实例来进行验证。

  • 打开全局缓存开关
1
2
3
4
<!-- 打开全局缓存开关 -->
<setting name="cacheEnabled" value="true"/>
<!-- 为了验证全局缓存,在这里把Session缓存关闭 -->
<setting name="localCacheScope" value="STATEMENT"/>
  • 定义全局缓存实例
1
2
3
// 在xml映射器中定义全局缓存
<!-- 在xml映射器中配置全局缓存 -->
<cache />

在xml映射器中配置全局缓存很简单,只需要在xml映射器中简单添加一个<cache />节点即可,这里为了演示全局缓存的效果,所以不用配置详细参数,使用默认值即可。

1
2
3
// 在接口映射器中定义全局缓存
@CacheNamespace
public interface StudentMapper {

在接口映射器中配置全局缓存通过注解@CacheNamespace实现,其效果与在xml映射器中通过节点<cache />配置是一样的。

  • 验证全局缓存的作用

通过参数cacheEnabled控制的缓存是全局的,所以在多个Session中使用相同SQL语句查询同一条数据时,只在第一次查询时直接查询数据库,之后的查询都会从这个全局缓存中读取数据。如下以通过xml映射器查询为例:

1
2
3
4
5
6
7
8
9
10
// 在不同Session中查询同一条数据
SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
Student student = sqlSession1.selectOne("org.chench.test.mybatis.mapper.getStudentById", 1);
System.out.println(student);
sqlSession1.close();

SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
student = sqlSession2.selectOne("org.chench.test.mybatis.mapper.getStudentById", 1);
System.out.println(student);
sqlSession2.close();

查看MyBatis的输出日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
method: query
DEBUG [main] - Cache Hit Ratio [org.chench.test.mybatis.mapper]: 0.0
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 1463355115.
DEBUG [main] - ==> Preparing: select * from test where id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
Student{id=1, name='1509690042107_haha_update_update', age=0, sex=0}
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@573906eb]
DEBUG [main] - Returned connection 1463355115 to pool.
method: query
DEBUG [main] - Cache Hit Ratio [org.chench.test.mybatis.mapper]: 0.5
Student{id=1, name='1509690042107_haha_update_update', age=0, sex=0}

显然,从日志中很明显看到第一次查询时缓存命中率为0,第二次查询时缓存命中率为0.5,直接从缓存中取得了数据。

总结

MyBatis的缓存功能同时受到localCacheScope和cacheEnabled这2个运行时参数的控制。那么我们不禁要问:为什么需要使用2个参数进行控制而不是直接使用1个参数更加简洁?实际上,2个参数控制的缓存策略是不一样,localCacheScope参数控制的缓存是Session范围内的,称为一级缓存;而cacheEnabled参数控制的缓存是全局的,称为二级缓存,这对应于不同的应用需求。

显然,MyBatis的默认配置是同时开启了Session缓存和全局缓存。另外请注意:cacheEnabled参数仅仅是打开了全局缓存开关,但这并不意味着默认情况下MyBatis就会进行全局缓存。实际上,如果需要使用全局缓存,还必须在映射器中配置全局缓存实例

【参考】
深入浅出mybatis之缓存机制