mybatis 緩存(三)

mybatis的緩存分爲一級緩存和二級緩存java

一級緩存:基於SqlSession級別的緩存,也就是說,緩存了這個SqlSession執行全部的select.MapperStatement的結果集;同一個查詢語句,只會請求一次;可是當前SqlSession執行增刪改操做或者commit/rollback操做時,會清空SqlSession的一級緩存;sql

禁止一級緩存(同理也禁止了二級緩存)數據庫

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方法纔會生效session

禁止二級緩存:mybatis

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出現多表查詢時,會引發髒數據,如:app

  • A namespace中的mapperStatement id = "xxx"查詢了表A id=1和表B id=5的數據,二級緩存生效
  • 表B 更新了id=5的數據;

此時再來查A表中的mapperStatement id = "xxx"時,仍是使用了A namespace中的二級緩存;這引發了B表id=5的髒數據ide

固然解決上述問題能夠使用<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共享

相關文章
相關標籤/搜索