myBatis是一个常用的java数据库访问的持久层框架,它支持定制化 SQL、存储过程以及高级映射。
最近在项目中遇到了mybatis缓存的一些问题,所以在这里整理了下,与大家分享,也欢迎大家一起多交流
一级缓存
结构
缓存这东西,大家都清楚,就是在client执行多次相同的sql的时候,防止穿透,增加响应速度,减少DB压力
通常我们一次write或者read请求都会new一个SqlSession,在mybatis里,sqlSession是一个接口类,它的实现类DefaultSqlSession,每一个sqlSession持有一个executor,BaseExecutor里持有一份localCache,localCache底层就是使用一个Map<Object,Object>来存储的,那基本流程相信大家也基本猜到了,MyBatis来查询的时候,根据语句生成MappedStatement,然后在localCahe里查询,如果命中直接返回,如果没有命中,则查询数据库,再将结果写入localCache
一级缓存属于SqlSession级别的,一个SqlSession可以理解为一次数据库会话,如果新建一个SqlSession,则之前的缓存不会生效(即一级缓存生效范围在同一个SqlSession内),如果SqlSession执行任何一次update操作,则之前的localCache也会失效
@Override
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);
}
这里update之后,直接clearLocalCache(),即Map.clear()
如何保证缓存的命中
既然底层是Map<k,v>来存储的,那么缓存的命中就取决于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();
if (parameterMappings.size() > 0 && parameterObject != null) {
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
cacheKey.update(parameterObject);
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
for (ParameterMapping parameterMapping : parameterMappings) {
String propertyName = parameterMapping.getProperty();
if (metaObject.hasGetter(propertyName)) {
cacheKey.update(metaObject.getValue(propertyName));
} else if (boundSql.hasAdditionalParameter(propertyName)) {
cacheKey.update(boundSql.getAdditionalParameter(propertyName));
}
}
}
}
return cacheKey;
}
CacheKey的决定因素
1、statementId
2、rowBounds.offset
3、rowBound.limit
4、sql语句
5、sql参数
这里的update()函数,实际是计算一个参数的hashcode,做一个校验和
public void update(Object object) {
int baseHashCode = object == null ? 1 : object.hashCode();
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
updateList.add(object);
}
意义
一级缓存底层就是使用一个HashMap来实现的缓存,前提是预设该缓存存在的时间很短。其中的问题就是如果长时间使用一个SqlSession,那么这个hashMap的缓存会越来越大,同时缓存的时效性也会越来越弱。虽然Cache提供了缓存失效的接口,但是并不是强制的。任何update操作时也会清空缓存,但也有可能出现DB数据修改了上层没有感知的情况。所以要严格控制SqlSession的生命周期
流程
首先通过DefaultSqlSessionFactory.openSession
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
······
final Executor executor = configuration.newExecutor(tx, execType, autoCommit);
return new DefaultSqlSession(configuration, executor);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
这里会new一个Executor,根据executorType来生成不同的Executor的实现类,默认的defaultExecutorType是SIMPLE类型
public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
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, autoCommit);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
创建完executor,这里有个cacheEnabled参数,默认为true,会为executor套上一层装饰类,这个与二级缓存有关,之后再讲。
SqlSession会把具体的职责委托给Executor,只开启一级缓存的时候,委托会给BaseExecutor,看下BaseExecutor的query方法
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);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
生成CacheKey,然后去localCache获取,如之前所说,如果没有命中则去DB里查询
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache(); // issue #482
}
最后有一条判断,如果localCache是statement级别的,那么直接清空localCache,也就是statement级别的一级缓存是无法共享localCache的
<setting name="localCacheScope" value="STATEMENT"/>
所以在分布式或者多SqlSession上建议使用statement缓存级别,避免在数据库写入时导致其他session的脏读
二级缓存
之前提到一级缓存是SqlSession级别的,而二级缓存的作用域是全局的。一级缓存是默认打开的,而二级缓存需要通过一定的配置(配置这里不具体讲了,大家可以自己看使用文档)
结构与流程
上文提到的BaseExecutor的装饰类CacheExecutor,当有请求来的时候,CacheExecutor会先判断二级缓存是否命中,之前看到有一副不错的流程图,这里拿来分享下:
在CacheExecutor每次query时,先会去获取Cache,每个CacheExecutor持有一个TransactionalCacheManager对象,而TransactionalCacheManager持有一个Map对象,其中存储了Cache和TransactionalCache的映射关系。
TransactionalCache是一个实现了Cache接口的包装类,只有在事务提交后缓存才会生效
查询与写入
再回来看CacheExecutor的query方法
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) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, key, parameterObject, boundSql);
if (!dirty) {
cache.getReadWriteLock().readLock().lock();
try {
@SuppressWarnings("unchecked")
List<E> cachedList = (List<E>) cache.getObject(key);
if (cachedList != null) return cachedList;
} finally {
cache.getReadWriteLock().readLock().unlock();
}
}
List<E> list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578. Query must be not synchronized to prevent deadlocks
return list;
}
}
return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
如果MappedStatement中对应的Cache存在,并且对于的查询开启了二级缓存(useCache="true"),那么在CachingExecutor中会先从缓存中根据CacheKey获取数据,如果缓存中不存在则从数据库获取。获取后tcm.putObject放入entriesToAddOnCommit(只有当commit的时候才会再次读取生效)
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
}
总结:
二级缓存很容易出现脏数据,虽然在update时候会通过dirty字段强制更新,但在多表查询中,还是极可能出现脏数据,所以在生产环境还是建议关闭。分布式环境下还是建议自己实现redis等缓存来实现