mybatis缓存(三)
mybatis的缓存分为一级缓存和二级缓存
一级缓存:基于SqlSession级别的缓存,也就是说,缓存了这个SqlSession执行所有的select.MapperStatement的结果集;同一个查询语句,只会请求一次;但是当前SqlSession执行增删改操作或者commit/rollback操作时,会清空SqlSession的一级缓存;
禁止一级缓存(同理也禁止了二级缓存)
xml方式:<select id="ping" flushCache="true" resultType="string">
...
</select>
注解方式:
@Options(flushCache = FlushCachePolicy.TRUE)
一级缓存导致的问题:每个SqlSession可能会对同一个mapperStatement缓存不同的数据,如:
- sqlSession1 查询了userId=1的数据,一级缓存生效
- sqlSession2更新了userId=1的name值,然后在查询,一级缓存生效
这导致了sqlSession1和sqlSession2对于userId=1的缓存数据不一致,引入脏数据
一级缓存源代码:
public abstract class BaseExecutor implements Executor { // 一级缓存
protected PerpetualCache localCache;
protected BaseExecutor(Configuration configuration, Transaction transaction) {
...
// 默认使用PerpetualCache
this.localCache = new PerpetualCache("LocalCache");
...
}
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
...
// 增删改删除一级缓存
clearLocalCache();
...
}
@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
...
// flushCache=true时清空一级缓存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
...
// 判断一级缓存是否有值
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);
}
...
}
@Override
public boolean isCached(MappedStatement ms, CacheKey key) {
// 一级缓存是否包含cachekey
return localCache.getObject(key) != null;
}
@Override
public void commit(boolean required) throws SQLException {
...
// commit 删除缓存
clearLocalCache();
...
}
@Override
public void rollback(boolean required) throws SQLException {
...
// rollback删除缓存
clearLocalCache();
...
}
@Override
public void clearLocalCache() {
if (!closed) {
// 清空缓存
localCache.clear();
localOutputParameterCache.clear();
}
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
...
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 删除原来的一级缓存
localCache.removeObject(key);
}
// 将新获取的值放入一级缓存
localCache.putObject(key, list);
。。。
}
}
二级缓存:基于SqlSessionFactory缓存,所有SqlSession查询的结果集都会共享;其实这样描述是不准确的,二级缓存是基于namespace的缓存,每个mapper对应一个全局Mapper namespace;当第一个Sqlsession查出的结果集,缓存在namespace中,第二个sqlsession再查找时会从nameSpace中获取;每个namespace是单例的;只有sqlSession调用了commit方法才会生效
禁止二级缓存:
mybatis-congif.xml:将所有的namespace都关闭二级缓存 <settings>
<setting name="cacheEnabled" value="false"/>
...
</settings>
对单个namespace是否使用二级缓存
<cache /> 当前namespace是否使用二级缓存
<cache-ref namespace="..." /> 当前namespace和其它namespace共用缓存
对一个namespace中的单个MapperStatement关闭二级缓存
<select id="selectParentBeans" resultMap="ParentBeanResultMap" useCache="false">
select * from parent
</select>
二级缓存导致的问题:当某个namespace出现多表查询时,会引起脏数据,如:
- A namespace中的mapperStatement id = "xxx"查询了表A id=1和表B id=5的数据,二级缓存生效
- 表B 更新了id=5的数据;
此时再来查A表中的mapperStatement id = "xxx"时,还是使用了A namespace中的二级缓存;这引起了B表id=5的脏数据
当然解决上述问题可以使用<cache-ref>;但这会导致缓存粒度变粗,多个namespace的操作都会影响该缓存;
二级缓存源码:
Configuration#public
Executor newExecutor(Transaction transaction, ExecutorType executorType) {...
// 判断是否使用二级缓存
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 对executor制定插件
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
public class CachingExecutor implements Executor { // 委托设计模式
private final Executor delegate;
// 二级缓存
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
// 更新调用清空缓存方法(flushCache=false时不情况二级缓存)
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 判断该namespace是否含有缓存
Cache cache = ms.getCache();
if (cache != null) {
// 看是否需要清空该namespace下的二级缓存
flushCacheIfRequired(ms);
// 当前MapperStatement是否需要使用二级缓存
if (ms.isUseCache() && resultHandler == null) {
...
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 存入二级缓存
tcm.putObject(cache, key, list); // issue #578 and #116
...
}
}
}
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
// flushCache=true时清空该namespace下的二级缓存,反之则不情况缓存
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
}
public class TransactionalCacheManager {// 存储二级缓存,以namespace的cache作为key
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
// 清空namespace的缓存
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
// 获取某个namespace中的cacheKey的缓存
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
// 放入某个namespace cacheKey的缓存
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
// 二级缓存提交
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
// 二级缓存回滚
public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}
// transactionalCaches缓存有则不存储,没有则存入
private TransactionalCache getTransactionalCache(Cache cache) {
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
}
在看源代码的时候,transactionalCaches引起深思
- 每个sqlSession中有新的CachingExecutor
- 每个CachingExecutor有新的TransactionalCacheManager
- TransactionalCacheManager中的transactionalCaches是每个sqlSession独享的,如何达到线程安全且多个sqlSession共享呢?
其实二级缓存是以namespace粒度存储在Mapper里面的;每个mapper是全局共享的;而且getTransactionalCache这个方法已经将当前的namespace存放于每个cachingExecutor中了,所以达到了线程安全且sqlSession共享
以上是 mybatis缓存(三) 的全部内容, 来源链接: utcz.com/z/514825.html