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

回到顶部