这是我参与8月更文挑战的第31天,活动详情查看:8月更文挑战
使用及配置
-
使用方式见Mybatis之缓存、懒加载
-
一级缓存的作用域
-
mybatis-config.xml中的配置
<setting name="localCacheScope" value="SESSION"/> -
对应枚举类
// 本地缓存作用域 public enum LocalCacheScope { SESSION, // 本地缓存的作用域为一次回话 STATEMENT // 本地缓存的作用域为一条语句,也就是不进行任何缓存 }
-
关键代码
缓存执行的流程
在BaseExecutor的query方法(不带CacheKey和BoundSql参数)中,调用createCacheKey方法对一级缓存的key进行了生成。
// 创建CacheKey,并将所有查询参数依次更新写入
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
cacheKey.update(value);
}
}
cacheKey.update(configuration.getEnvironment().getId());
由关键代码可以看出,key是由MappedStatement的id,查询的RowBounds参数,BoundSql,参数映射以及环境配置共同组成的。
在生成了缓存的key后,进入到BaseExecutor的query方法(带CacheKey和BoundSql参数)中,首先根据key从一级缓存中查询。如果查询到了数据,则不再查询数据库,直接返回。如果没有查询到,则进入数据库查询。
// 尝试从本地缓存获取结果
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
// 本地缓存中有结果,则对于CALLABLE语句还需要绑定到IN/INOUT参数上
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 本地缓存没有结果,故需要查询数据库
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
// 如果本地缓存的作用域为STATEMENT,则立刻清除本地缓存
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
在query方法的最后,如果当前MappedStatement查询结束,则会判断一级缓存的作用域,如果作用域为STATEMENT,则会清除一级缓存。
在查询数据库的queryFromDatabase方法中,查询到结果后,会将查询结果写入到一级缓存中。
// 向缓存中增加占位符,表示正在查询
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 使用具体的Executor执行查询
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 删除占位符
localCache.removeObject(key);
}
// 将查询结果写入缓存
localCache.putObject(key, list);
缓存的生命周期
在BaseExecutor类中,可以看到localCache是它的一个成员变量,并且在构造方法中进行了初始化,那么localCache的创建就是与BaseExecutor的创建是一起的。
// 查询操作的结果缓存
protected PerpetualCache localCache;
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.localCache = new PerpetualCache("LocalCache");
}
而在Mybatis源码之创建SqlSession对象一文中可以看到,在通过openSession方法创建SqlSession对象时,就会通过Configuration对象创建Executor。
// 创建执行器
final Executor executor = configuration.newExecutor(tx, execType);
所以说,一级缓存最大的作用域是不能跨SqlSession的。
存在的问题
缓存最容易出现的问题就是更新不及时导致的脏读。下面列举两种一级缓存中可能会出现的问题:
不同SqlSession操作相同的数据
sequenceDiagram
autonumber
SqlSession1 ->> DB : read
DB -->> SqlSession1 : data1
SqlSession1 ->> SqlSession1 : cache data1
SqlSession2 ->> DB : update data1 to data1+
SqlSession1 ->> SqlSession1 : read cache data1
在上述情况中,步骤5读取的数据是缓存中的数据,而真实数据已经在步骤4中被修改了,因此产生了脏读。
解决这种情况,可以将一级缓存的作用域修改为STATEMENT,修改后的步骤5就会去读DB中的最新数据,避免脏读。
一级缓存的作用域为STATEMENT的情况下,使用嵌套查询
上文提到,在当前MappedStatement查询结束后,会判断一级缓存的作用域是否为STATEMENT,如果是,则清除缓存,否则不会清除缓存。
判断当前MappedStatement查询是否结束,是根据queryStack参数是否为0来判断的,当执行嵌套内部查询时,外部查询还未结束,因此内部查询在使用一级缓存时,仍然会存在脏读的问题。
sequenceDiagram
autonumber
SqlSession1 ->> DB : 嵌套外部查询, queryStack=1
SqlSession1 ->> DB : 嵌套内部查询,queryStack=2
DB -->> SqlSession1 : 返回嵌套内部查询,并cache,queryStack=1,不会清除cache
SqlSession2 ->> DB : update data
SqlSession1 ->> SqlSession1 : 嵌套内部查询,走cache,queryStack=1
DB -->> SqlSession1 : 嵌套外部查询结束,queryStack=0,清除缓存
由于嵌套查询需要多次发SQL去数据库查询数据,因此嵌套内部查询仍然可以使用缓存,如果按上图的步骤4步骤5来走,那后续的嵌套内部查询都会出现脏读的情况。
解决这种情况,可以禁用一级缓存,flushCache="true",或者在Mybatis的查询中不使用嵌套查询。
以上就是Mybatis源码中一级缓存的介绍。