一、MyBatis缓存介绍
缓存的使用可以明显的加快访问数据速度,提升程序处理性能,生活和工作中,使用缓存的地方很多。在开发过程中,从前端-->后端-->数据库等都涉及到缓存。MyBatis作为数据访问框架,也提供了缓存功能,分别为:一级缓存和二级缓存。在使用MyBatis的时候,显示或者默认的使用了缓存。缓存虽好,但是如果随便使用,可能会导致很多问题,因此,有必要了解MyBatis缓存的底层工作原理。
二、一级缓存
1、工作原理
在应用程序运行时,在一个数据库事务中,很可能一个查询SQL运行多次,这里就可以直接从缓存中拿数据,而不必去查询数据库。因此,这里MyBatis就设置了一级缓存,来提高查询效率。其流程如下:
对应的时序图如下:
缓存未命中时序图:
缓存命中:
缓存key的生成规则为:MappedStatement.id+offset+limit+sql+property+environment,只有在key完全相同的情况下,才会被认为是命中。
cacheKey的一个样例值:
461375566:-715040544:com.iwill.mybatis.dao.mapper.ext.UserMapperExt.findUserListByName:0:2147483647:SELECT * FROM CNT_USER WHERE name =?:zhangsan:SqlSessionFactoryBean
cacheKey的生成源码如下:
@Override
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;
}
一级缓存的查询代码如下:
@SuppressWarnings("unchecked")
@Override
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();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
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;
}
2、注意事项
a、一级缓存默认是开启的,且其适用范围为SESSION,其使用范围是同一个SqlSession中的同一个事务中,跨事务的查询是不会走到一级缓存的,因为在第一个事务提交(还有insert、update、delete等操作)时,会执行clearLocalCache操作,这里面会去清空一级缓存。
b、默认的一级缓存存在一个问题,如果在同一个事务A中,两个相同SQL执行中间,另外一个事务提交了数据,那么事务A中的第二次查询就会读取到脏数据(直接读缓存,未读取数据库)。对数据敏感的业务,需要设置一级缓存的适用范围为STATEMENT,这样,每次执行一个sql,都会清空一级缓存(相当于关闭了一级缓存),下次查询时,就会去查询数据库了。
三、二级缓存
1、工作原理
二级缓存是范围更广的缓存,其适用范围是一个namespace(这个namespace就是一个mapper xml文件),也可以多个namespace共享一个二级缓存。其执行流程如下:
二级缓存执行代码如下:
@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) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
默认情况下,二级缓存是关闭的,需要主动打开二级缓存,配置方式如下:
a、在mybatis-config.xml中设置cacheEnabled=true(默认情况下,cacheEnabled也是true):
<settings>
<setting name="localCacheScope" value="SESSION"/>
<setting name="cacheEnabled" value="true"/>
<!-- <!–开启驼峰式命名,数据库的列名能够映射到去除下划线驼峰命名后的字段名–>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="logImpl" value="LOG4J"/>-->
</settings>
b、在mapper xml中配置cache标签
<cache/>
2、注意事项
a、二级缓存是范围更加广泛的缓存,针对namespace级别,也可以跨namespace,这样也更加容易产出脏数据,因此,一般不会开启二级缓存。
b、二级缓存默认关闭,如果需要打开,则需要单独配置,如果需要打开所有namespace的二级缓存,目前MyBatis还不支持这样的配置,可以进行优化(意义可能不大,因为二级缓存使用的少)。
四、写在最后
缓存就如一把双刃剑,有优点也有缺点,关键在于怎么使用。优点就是可以提高数据访问速度,缺点就是缓存中数据可能和实际的数据(数据库、远程应用数据等)不一致。俗话说,任何脱离业务场景的架构都是耍流氓,对于缓存的使用,同样适用。
我们应该结合我们具体的业务场景来使用缓存,针对本文,MyBatis的一二级缓存,如果数据只查询,不更新,那么完全没问题。
项目地址:https://github.com/yangjianzhou/mybatis-demo
参考:
2、mybatis