MyBatis提供了一级缓存和二级缓存,其中一级缓存基于SqlSession实现,而二级缓存基于Mapper实现。
MyBatis一级缓存
概述
Mybatis一级缓存默认是开启的,而且不能关闭。至于一级缓存为什么不能关闭,MyBatis核心开发人员做出了解释:MyBatis的一些关键特性(例如通过和建立级联映射、避免循环引用(circular references)、加速重复嵌套查询等)都是基于MyBatis一级缓存实现的。
MyBatis提供了一个配置参数localCacheScope
,用于控制一级缓存的级别,该参数的取值为SESSION
、STATEMENT
。
- 当指定
localCacheScope
参数值为SESSION
时,缓存对整个SqlSession有效,只有执行DML语句(更新语句)时,缓存才会被清除。 - 当
localCacheScope
值为STATEMENT
时,缓存仅对当前执行的语句有效,当语句执行完毕后,缓存就会被清空。
实现原理
SqlSession提供了面向用户的API,但是真正执行SQL操作的是Executor组件。Executor采用模板方法设计模式,BaseExecutor
类用于处理一些通用的逻辑,其中一级缓存相关的逻辑就是在BaseExecutor
类中完成的
在BaseExecutor
类中维护了两个PerpetualCache属性,代码如下:
public abstract class BaseExecutor implements Executor {
// Mybatis一级缓存对象
protected PerpetualCache localCache;
// 存储过程输出参数缓存
protected PerpetualCache localOutputParameterCache;
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<>();
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
}
BaseExecutor
的query()
方法相关的执行逻辑,代码如下:
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
// 构建缓存cacheKey
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 执行查询
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 {
// 是否清空localCache,flushCache="true"
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 {
// 缓存不存在从数据库中获取数据,然后回填到localCache;
// 如果执行语句为存储过程,还需回填到localOutputParameterCache。
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
// 当localCacheScope = STATEMENT时,清空缓存。
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
MyBatis一级缓存的“坑”
在实际生产中务必将MyBatis的localCacheScope属性设置为STATEMENT,避免其他应用节点执行SQL更新语句后,本节点缓存得不到刷新而导致的数据一致性问题。
MyBatis二级缓存
如何使用MyBatis二级缓存?
MyBatis二级缓存的使用比较简单,只需要以下几步:
-
在MyBatis主配置文件中指定
cacheEnabled
属性值为true。<settings> ... <setting name="cacheEnabled" value="true"/> </settings>
-
在MyBatis Mapper配置文件中,配置缓存策略、缓存刷新频率、缓存的容量等属性。例如:
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
-
在配置Mapper时,通过useCache属性指定Mapper执行时是否使用缓存。另外,还可以通过flushCache属性指定Mapper执行后是否刷新缓存,例如:
<select id="list" flushCache="false" useCache="true" resultType="User"> select * from user </select>
通过上面的配置,MyBatis的二级缓存就可以生效了。执行查询操作时,查询结果会缓存到二级缓存中,执行更新操作后,二级缓存会被清空。
实现原理
MyBatis二级缓存是通过CachingExecutor
实现的。Configuration
类提供了一个工厂方法newExecutor()
,该方法返回一个Executor
对象。代码如下:
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);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
如果cacheEnabled
属性值为true
(开启了二级缓存),则使用CachingExecutor
对普通的Executor
对象进行装饰,CachingExecutor
在普通Executor
的基础上增加了二级缓存功能。
CachingExecutor类中维护了一个TransactionalCacheManager
实例,TransactionalCacheManager
用于管理所有的二级缓存对象。CachingExecutor
代码如下:
最后,回顾一下MappedStatement对象创建过程中二级缓存实例的创建。XMLMapperBuilder在解析Mapper配置时会调用cacheElement()
方法解析标签,cacheElement()方法代码如下:
在调用MapperBuilderAssistant
对象的addMappedStatement()
方法创建MappedStatement
对象时会将当前命名空间对应的二级缓存对象的引用添加到MappedStatement
对象中。
MyBatis使用Redis缓存
MyBatis官方提供了一个mybatis-redis模块,该模块用于整合Redis作为二级缓存。使用步骤如下:
-
首先需要引入该模块的依赖;
-
然后需要在Mapper的XML配置文件中添加缓存配置。如下所示:
<cache type = "org.mybatis.caches.redis.redisCache" />
-
最后,需要在classpath下新增redis.properties文件,配置Redis的连接信息。
参考资料
- 美团技术团队-聊聊MyBatis缓存机制
- MyBatis 3源码深度解析