目录
一、介绍
对DB的访问相对于对内存的访问耗时明显,缓存作为优化系统性能的常用手段,Mybatis也引入缓存以减少对DB的重复访问。Mybatis内部支持一级缓存(SqlSession级别)和二级缓存(Application级别),下面将详细介绍这两种缓存。
二、一级缓存
2.1 简要介绍
一级缓存是SqlSession级别的缓存,一级缓存默认开启。SqlSession是使用Mybatis时主要的类,内部提供了执行sql、获取mapper、管理事务的方法。一级缓存由BaseExecutor.localCache维护,Executor会先去查询一级缓存,如果命中则返回;否则查询db并更新缓存。这些核心组件之间的流程图如下所示:
下面介绍查询流程、更新流程、为什么说一级缓存是SqlSession级别的缓存。
2.2 查询流程
一级缓存由PerpetualCache维护,而PerpetualCache类持有了Map<String, Object>的成员变量。查询的核心流程:
- BaseExecutor首先会根据mappedStatement(sql配置)、parameterObject(查询参数)、rowBounds(内存分页相关)、boundSql(待执行的实际sql)构建缓存key;
- 从localCache中查询缓存是否存在,如果存在则返回;
- 缓存未命中,查询db后更新缓存;
- 如果一级缓存的级别是STATEMENT(见localCacheScope属性,支持SESSION/STATEMENT两种配置),则更新缓存。
代码:BaseExecutor.query():
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); // 构建缓存key
return query(ms, parameter, rowBounds, resultHandler, key, boundSql); // 执行查询流程
}
// mybatis内部缓存key的构建流程,核心是怎么判断两个请求是同一个请求,感兴趣的可以自行分析
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());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
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) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
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(); // 如果mappedStatmenet配置了flushCache为true,那么每次执行查询前清空缓存
}
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;// 查缓存
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); // 和CallableStatement(应用于存储过程)相关,本文不做介绍
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); // 查DB,并更新缓存
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache(); // 如果LocalCacheScope是STATEMENT级别,那么执行完sql后清空缓存
}
}
return list;
}
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear(); // 清空缓存
localOutputParameterCache.clear();
}
}
总结下来时序图如下:
2.3 生命周期
通过Mybatis执行sql时,首先需要获取一个新的SqlSession(多线程下BaseExecutor.query()不安全),SqlSession会初始化一个新的Executor对象。SqlSession、Executor内部的一些方法都会触发到一级缓存的清理。
缓存清理主要有以下流程:
- 调用SqlSession.close()关闭会话时,sqlSession.executor.localCache会被置为null;
- 调用SqlSession.clearCache();
- 调用SqlSession.commit()提交会话时,一级缓存会被清空;
- 执行update、delete、insert等操作时,会先清空一级缓存。
具体代码相对简单,感兴趣可以自行阅读。
三、二级缓存
3.1 简要介绍
二级缓存是Application级别的缓存,缓存的粒度是mapper的namespace(同一个mapper使用同一个缓存),意思是服务启动成功后,除了显示地去清理二级缓存,否则二级缓存的生命周期和Application一致。二级缓存的读写和CachingExecutor相关,在Mybatis源码(3)-查询执行流程中简要介绍了CachingExecutor的执行流程,CachingExecutor代理了Executor实现类的执行,增加了二级缓存的功能。如下图所示,开启二级缓存的方法是设置属性为true,同时在mapp.xml文件中添加<cache/>元素,只有配置了这些后SqlSession持有的Executor才是CachingExecutor。
如下图所示,SqlSession在查询数据时,如果配置了二级缓存,首先会通过CachingExecutor查询是否命中缓存,如果没有则通过DelegateExecutor查询结果并更新二级缓存。
下面介绍二级缓存的查询流程和更新流程:
3.2 流程分析
核心代码在CachingExecutor中:
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) { // 如果配置了<cache/>标签
flushCacheIfRequired(ms); // 如果设置了flushCache == true则执行清空缓存操作
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key); // 查询二级缓存
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); // 查询一级缓存 -> dbn
tcm.putObject(cache, key, list); // 将缓存k-v放置在entriesToAddOnCommit中,等sqlsession.commmit()时才会真正将k-v写入到二级缓存中
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); // 查询一级缓存 -> db
}
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms); 设置clearOnCommit标志位,commit时根据该标志位判断是否清空缓存
return delegate.update(ms, parameterObject); // 执行BaseExecutor.update()
}
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required); // 通过BaseExecutor的实现类实现commit操作(提交事务,清理一级缓存)
tcm.commit(); // 执行TransactionCacheManager的commit操作,遍历所有namespace的二级缓存,执行其commit操作。
}
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) { // 如果配置了flushCache == true
tcm.clear(cache); // 只是设置clearOnCommit为true,并清空entriesToAddOnCommit
}
}
TransactionCacheManager: 基于事务的缓存管理器,mybatis的二级缓存无论新增/更新都只是设置TransactionalCache的clearOnCommit、entriesToAddOnCommit(待写入的缓存kv),entriesMissedInCache(miss的key列表)等属性,等执行sqlSession.commmit()时二级缓存才会变更。
TransactionCacheManager:
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
private TransactionalCache getTransactionalCache(Cache cache) {
return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
}
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit(); // 遍历各namespace的二级缓存,执行commit
}
}
Mybatis使用TransactionalCache管理二级缓存Cache的实现类。
TransactionalCache:
private final Cache delegate; // 二级缓存的实现类,默认实现类是PerpetualCache,用HashMap存放缓存对象
private boolean clearOnCommit; // commit时是否清空缓存的标志位
private final Map<Object, Object> entriesToAddOnCommit; // commit时待写入二级缓存的k-v列表
private final Set<Object> entriesMissedInCache; // miss的key列表
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
@Override
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}
public void commit() {
if (clearOnCommit) {
delegate.clear(); // 如果设置了clearOnCommit,则清空二级缓存
}
flushPendingEntries(); // 将entriesToAddOnCommit、entriesToAddOnCommit刷到cache中
reset(); // 重置clearOnCommit、entriesToAddOnCommit、entriesToAddOnCommit
}
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
至此二级缓存的分析完毕,核心组件:CachingExecutor代理了BaseExecutor的执行,增加了二级缓存管理、查询的功能。二级缓存的管理、查询则有TransactionCachingExecutor负责,二级缓存的写入、清理时都只是首先记录待写入的缓存kv、设置清理标志位,等cachingExecutor.commit时才会真正生效。
四、总结
mybatis支持一级缓存、二级缓存两种缓存,其中一级缓存是sqlSession级别的缓存,二级缓存则是application级别、每个namespace对应一个缓存对象。一级缓存、二级缓存由于其实现方式可能导致缓存不一致的现象,在分布式环境下该问题可能更突出,可能会导致不同机器上对同一请求的响应不一致,同时为了集中式地管理缓存,我们在生产环境中一般使用Redis等开源缓存数据库管理缓存。在mybatis的配置上,一般会关闭二级缓存、同时将localCacheScope设置为STATEMENT(每执行一次query都会清理一级缓存)。