MyBatis中的缓存
MyBatis
中主要包括一级缓存和二级缓存。一级缓存指的是 session
级别的缓存,MyBatis
每次创建一个数据库连接,则会产生一个数据库访问的 sqlSession
,在这一次会话中执行了两次相同的数据库查询的话,MyBatis
在第二次查询的时候会使用之前缓存的数据,从而提高查询效率;二级缓存指的是应用级别的缓存,开启缓存后,每一个 Mapper
下的查询都会使用缓存数据,二级缓存可以使用自定义实现,也可以使用第三方提供的缓存,如 EHCache
等。
一级缓存
一级缓存的实现
MyBatis
在进行一次查询时,主要包括如下几个步骤:
- 使用
SqlSessionFactoryBuilder
从XML
中读取配置信息,构造一个SqlSessionFactory
对象。 SqlSessionFactory
对象调用openSession
方法,开启一次数据库会话。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
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
40public 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认为,对于两次查询,如果以下条件都完全一样,那么就认为它们是完全相同的两次查询:
- 传入的
statementId
- 查询时要求的结果集中的结果范围 (结果的范围通过
rowBounds.offset
和rowBounds.limit
表示); - 这次查询所产生的最终要传递给JDBC
Java.sql.Preparedstatement
的Sql语句字符串(boundSql.getSql()
) - 传递给
java.sql.Statement
要设置的参数值
3、4两条MyBatis
最本质的要求就是 : 调用JDBC
的时候,传入的SQL语句要完全相同,传递给JDBC
的参数值也要完全相同。
根据一级缓存的特性,在使用的过程中,我认为应该注意: - 对于数据变化频率很大,并且需要高时效准确性的数据要求,我们使用
SqlSession
查询的时候,要控制好SqlSession
的生存时间,SqlSession
的生存时间越长,它其中缓存的数据有可能就越旧,从而造成和真实数据库的误差;同时对于这种情况,用户也可以手动地适时清空SqlSession
中的缓存; - 对于只执行、并且频繁执行大范围的
select
操作的SqlSession
对象,SqlSession
对象的生存时间不应过长。
一级缓存的生命周期
一级缓存在 session
中创建,因此它的生命周期和session
的生命周期一致。随 session
而生,随 session
而死。同时注意,在执行更新操作时,也会清空该session
的一级缓存。
二级缓存
二级缓存可以看下《深入理解mybatis原理》 MyBatis的二级缓存的设计原理。主要讲解了二级缓存的配置使用以及原理。需要注意的:
- 缓存的使用顺序二级缓存 -> 一级缓存-> 数据库。
- 缓存策略有 FIFO、LRU、Scheduled(指定时间清空)。
- 二级缓存的作用域:以Mapper区分,通常一个Mapper一个Cache,也可以多个Mapper公用一个Cache对象。
- 二级缓存的实现有三种选择:
- MyBatis自身提供的缓存实现;
- 用户自定义的Cache接口实现(实现Cache接口,并在
<cache />
中指明) - 跟第三方内存缓存库的集成(如Ehcache等);
总结
MyBatis可以采用一级缓存和二级缓存;一级缓存不需要配置,默认开启,但是在一些实时性要求较高的应用可能需要手动清空缓存。二级缓存指的是应用级别的缓存,在每一个Mapper都对应一个Cache对象,也可以多个Mapper共有一个;在实际项目中,我们使用的是用Redis来实现自己的缓存。