本文基于mybatis-spring 1.3.1和mybatis 3.4.4版本
mybatis提供了两级缓存,一个在事务内部使用的一级缓存,另一个可以全局使用的二级缓存。
一、一级缓存
一级缓存是在SqlSession内部使用,也就是一级缓存的最大有效范围只能在事务内部。可以使用参数“localCacheScope”设置一级缓存, 一共有两个值:SESSION和STATEMENT。默认值为SESSION,会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,每次查询完之后都会清空一级缓存,使用STATEMENT相当于关闭了一级缓存。
下图是查询的执行流程:
每个SqlSession实例中都有一个Executor对象,Executor对象执行查询操作。
Executor有多个实现,无论哪种实现,查询的时候都会执行父类BaseExecutor的query方法。
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();
}
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
//如果设置localCacheScope=STATEMENT,则清空缓存
clearLocalCache();
}
}
return list;
}
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;
}
查询结果保存到BaseExecutor的localCache属性,localCache便是一级缓存。
protected PerpetualCache localCache;
下面来看一下PerpetualCache类。
public class PerpetualCache implements Cache {
//id表示该缓存的名字
private String id;
//保存数据库的查询结果
private Map<Object, Object> cache = new HashMap<Object, Object>();
}
PerpetualCache将数据库的查询结果保存在HashMap的value中。
一级缓存其实是使用HashMap实现的。
接下来看一下缓存的key是怎么创建的。
缓存的key使用CacheKey表示。mybatis使用createCacheKey创建CacheKey对象。
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());//Mapper接口的全限定类名+方法名
cacheKey.update(rowBounds.getOffset());//查询数据的偏移量
cacheKey.update(rowBounds.getLimit());//查询条数
cacheKey.update(boundSql.getSql());//原始SQL语句
//下面处理SQL语句的入参,将入参放入cacheKey
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
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) {
//将Environment的id放入cacheKey中
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
CacheKey集合了SQL语句、SQL参数、调用的Mapper方法名、SQL分页参数、Environment对象信息。当比较两个CacheKey是否相等时,上述这些数据都要参与比较,而且CacheKey对象哈希值也是基于这些数据生成的。因此CacheKey重写了hashCode(),equals()。
下面看一下cacheKey.update方法就可以明白hashCode(),equals()如何做的了。
//每次向CacheKey添加数据时,都要调用该方法
public void update(Object object) {
//计算入参对象的hash值
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
count++;
checksum += baseHashCode;
baseHashCode *= count;
//计算CacheKey对象哈希值
//hashCode()方法返回的就是hashcode值
//multiplier = 37
hashcode = multiplier * hashcode + baseHashCode;
//updateList是List对象,用于保存原始数据,比较两个CacheKey是否相等,要比较updateList中的数据
updateList.add(object);
}
最后分析一下一级缓存何时清理。
一级缓存的清理时间点如下:
- 事务提交
- 事务回滚
- 执行insert/delete/update方法。清空缓存是在执行这些方法前。
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//清空缓存
clearLocalCache();
//访问数据库执行更新操作
return doUpdate(ms, parameter);
}
二、二级缓存
二级缓存默认是关闭的,开启二级缓存需要两步操作。
- 设置cacheEnabled=true,默认是true;
- 如果是注解配置,需要在Mapper接口的类上添加注解@CacheNamespace;如果是XML配置,在XML映射文件中添加“<cache/>”,该标签位于<mapper/>标签下。不同namespace使用的缓存实例不同,可以使用<cache-ref/>标签引用其他namespace的缓存实例。比如:
<cache-ref namespace="mapper.StudentMapper"/>
mybatis为cache标签提供了多个参数:
- type:cache使用的类型,默认是PerpetualCache,这在一级缓存中提到过。
- eviction: 定义回收的策略,常见的有FIFO,LRU。
- flushInterval: 配置一定时间自动刷新缓存,单位是毫秒。
- size: 最多缓存对象的个数。
- readOnly: 是否只读,若配置可读写,则需要对应的实体类能够序列化。
- blocking: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。
缓存使用Cache接口表示,Cache接口有多个实现类:
这些类之间嵌套装饰,形成一个嵌套链条,共同实现cache标签的设置。比如FIFO类型的回收策略就是由FifoCache实现的。这个链条的最后一个对象是PerpetualCache。PerpetualCache在一级缓存已经介绍过,其内部是一个Map对象,所以二级缓存与一级缓存一样缓存数据也是存储在map对象中。
二级缓存是由CachingExecutor处理的。
首先来看一下如何创建CachingExecutor,下面代码是SqlSession实例创建过程:
private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
try {
//代码删减
final Transaction tx = transactionFactory.newTransaction(connection);
//创建Executor实例,newExecutor方法见下方
final Executor executor = configuration.newExecutor(tx, execType);
//创建SqlSession实例
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
//下面是configuration.newExecutor方法的代码
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
//如果设置cacheEnabled为true,则创建CachingExecutor对象装饰其他Executor对象
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
因为CachingExecutor包装了其他Executor对象,而且位于最外层,因此SqlSession调用Executor查询数据库时,首先会调用到CachingExecutor中的方法。
下面是CachingExecutor中的查询方法:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
//创建CacheKey对象,作为缓存的key,一级缓存中已经介绍过
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
//调用查询方法
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
//检查映射文件是否配置了<cache/>标签
if (cache != null) {
//如果是增删改,flushCacheIfRequired将清空缓存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);
@SuppressWarnings("unchecked")
//查看缓存中是否有需要的数据
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
//如果缓存中没有对应数据,则执行下层Executor对象
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//将数据库返回结果放入缓存
tcm.putObject(cache, key, list);
}
return list;
}
}
//如果没有配置<cache/>标签,直接调用下层Executor对象
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
查询时首先检查二级缓存,当缓存中不存在时,才查询数据库。
从上面代码也可以看到执行增删改,会清空对应namespace下的二级缓存。
二级缓存与一级缓存还有一点不同,一级缓存只要查询结束就将结果放入缓存中,下次查询时就可以使用了,而二级缓存是将查询结果放入临时Map中,临时Map对查询不可见,当事务提交时,才将临时Map数据放入缓存中。
三、总结
- MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
- MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
- 在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。
四、引用
聊聊MyBatis缓存机制:https://tech.meituan.com/2018/01/19/mybatis-cache.html