从源码层面看MyBatis的缓存机制

MyBatis中的缓存

MyBatis 中主要包括一级缓存和二级缓存。一级缓存指的是 session 级别的缓存,MyBatis 每次创建一个数据库连接,则会产生一个数据库访问的 sqlSession ,在这一次会话中执行了两次相同的数据库查询的话,MyBatis在第二次查询的时候会使用之前缓存的数据,从而提高查询效率;二级缓存指的是应用级别的缓存,开启缓存后,每一个 Mapper 下的查询都会使用缓存数据,二级缓存可以使用自定义实现,也可以使用第三方提供的缓存,如 EHCache 等。

一级缓存

一级缓存的实现

MyBatis 在进行一次查询时,主要包括如下几个步骤:

  1. 使用SqlSessionFactoryBuilderXML 中读取配置信息,构造一个 SqlSessionFactory 对象。
  2. SqlSessionFactory 对象调用 openSession 方法,开启一次数据库会话。
  3. session 中持有了一个 Executor 的引用用来执行数据库操作。

在上述描述中,查询缓存是否命中是在 executor 执行 sql 时进行的。每个executor 中都包含一个PerpetualCache的缓存,该缓存使用一个简单的HashMap 来实现。在进行查询时首先会查询缓存有没有命中,如果命中,则直接返回结果;否则,从数据库中进行查询并放到缓存中。
Executor在执行查询时的代码如下:

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
 @Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
//创建缓存的key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
//调用下面的query方法
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
//omitted...
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--;
}
//omitted...
}

缓存的key的创建使用的是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
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
//添加MappedStatement的id
cacheKey.update(ms.getId());
//添加rowBound信息,分页时用的
cacheKey.update(Integer.valueOf(rowBounds.getOffset()));
////添加rowBound信息,分页时用的
cacheKey.update(Integer.valueOf(rowBounds.getLimit()));
//添加sql信息
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
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);
}
}
//如果Configuration的环境不为空,则把环境的id也加上
if (configuration.getEnvironment() != null) {
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;

从代码中可以看出,缓存的key由以下几部分组成。

  • MappedStatement对象的id
  • 分页信息RowBound
  • sql 语句。
  • sql语句中传递的参数
  • 环境名environment
    最后生成的key大约长这样:
    1
    -1994665822:3056142341:com.yrb.mybatis.mapper.BookStoreMapper.getBookNames:0:2147483647:select book_name from my_test.book_store where id = ?:4:development

一级缓存的使用和生命周期

一级缓存的使用

一级缓存默认开启,不过可以进行手动清除;MyBatis认为,对于两次查询,如果以下条件都完全一样,那么就认为它们是完全相同的两次查询:

  1. 传入的 statementId
  2. 查询时要求的结果集中的结果范围 (结果的范围通过rowBounds.offsetrowBounds.limit表示);
  3. 这次查询所产生的最终要传递给JDBC Java.sql.Preparedstatement的Sql语句字符串(boundSql.getSql()
  4. 传递给 java.sql.Statement 要设置的参数值
    3、4两条MyBatis最本质的要求就是 : 调用JDBC的时候,传入的SQL语句要完全相同,传递给JDBC的参数值也要完全相同。
    根据一级缓存的特性,在使用的过程中,我认为应该注意:
  5. 对于数据变化频率很大,并且需要高时效准确性的数据要求,我们使用SqlSession 查询的时候,要控制好SqlSession 的生存时间,SqlSession 的生存时间越长,它其中缓存的数据有可能就越旧,从而造成和真实数据库的误差;同时对于这种情况,用户也可以手动地适时清空 SqlSession 中的缓存;
  6. 对于只执行、并且频繁执行大范围的 select操作的 SqlSession 对象,SqlSession 对象的生存时间不应过长。

一级缓存的生命周期

一级缓存在 session 中创建,因此它的生命周期和session 的生命周期一致。随 session而生,随 session而死。同时注意,在执行更新操作时,也会清空该session的一级缓存。

二级缓存

二级缓存可以看下《深入理解mybatis原理》 MyBatis的二级缓存的设计原理。主要讲解了二级缓存的配置使用以及原理。需要注意的:

  • 缓存的使用顺序二级缓存 -> 一级缓存-> 数据库
  • 缓存策略有 FIFOLRUScheduled(指定时间清空)。
  • 二级缓存的作用域:以Mapper区分,通常一个Mapper一个Cache,也可以多个Mapper公用一个Cache对象。
  • 二级缓存的实现有三种选择:
    • MyBatis自身提供的缓存实现;
    • 用户自定义的Cache接口实现(实现Cache接口,并在<cache />中指明)
    • 跟第三方内存缓存库的集成(如Ehcache等);

总结

MyBatis可以采用一级缓存二级缓存;一级缓存不需要配置,默认开启,但是在一些实时性要求较高的应用可能需要手动清空缓存。二级缓存指的是应用级别的缓存,在每一个Mapper都对应一个Cache对象,也可以多个Mapper共有一个;在实际项目中,我们使用的是用Redis来实现自己的缓存。

参考资料