天天看點

Mybatis源碼分析(緩存)介紹一級緩存二級緩存

Mybatis緩存

  • 介紹
  • 一級緩存
    • 例子
    • 結論
    • 底層實作
      • PerpetualCache說明
      • CacheKey說明
      • LocalCacheScope說明
  • 二級緩存
    • 例子
    • 結論
    • 底層實作
      • TransactionalCacheManager說明
      • TransactionalCache說明
      • 為什麼我們需要送出,二級緩存才有用

介紹

mybatis緩存分一級緩存和二級緩存,其中一級緩存是sqlSession層面的緩存,二級緩存是Mapper層面的緩存。他們都是儲存在Jvm裡的,也就是java對象裡,是以在分布式部署的時候我們就需要實時更新緩存,不然可能會導緻資料一緻性的問題。

一級緩存

例子

上面我們說了一級緩存是sqlSession層面的緩存,現在我們來做個實驗證明一下。

// 啟動相同的兩條查詢,将sql列印出來,看查詢了幾次
 public static void main(String[] args){
     SqlSession session = sqlSessionFactory.openSession();
    // session.selectOne("selectCorp");
     UserMapper mapper = session.getMapper(UserMapper.class);
     UserDO userDO1 = mapper.getCorpByCorpId("261");
     UserDO userDO2 = mapper.getCorpByCorpId("261");
 }
           

從列印結果可以看出隻執行了一次

Mybatis源碼分析(緩存)介紹一級緩存二級緩存
// 我們分别建立兩個sqlSession來執行,看列印幾次
 public static void main(String[] args){
        SqlSession session1 = sqlSessionFactory.openSession();
        SqlSession session2 = sqlSessionFactory.openSession();
       // session.selectOne("selectCorp");
        UserMapper mapper1 = session1.getMapper(UserMapper.class);
        UserMapper mapper2 = session2.getMapper(UserMapper.class);
        UserDO userDO1 = mapper1.getCorpByCorpId("261");
        UserDO userDO2 = mapper2.getCorpByCorpId("261"); 
   }
           

從列印結果看出執行了兩次

Mybatis源碼分析(緩存)介紹一級緩存二級緩存

結論

當隻建立了一個sqlSession執行兩條相同的sql的時候,隻會查詢一次,當建立兩個sqlSession的時候會分别查詢,說明一級緩存的生命周期和SqlSession一緻

底層實作

我們主要來看BaseExecutor基礎執行器裡的query方法,來說明一級緩存的實作原理

@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++;
       //先通過key去取緩存中的集合,如果有就直接傳回,沒用就查詢
      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;
  }
           

PerpetualCache說明

我們看到我們的緩存是從localCache擷取的,也就是從PerpetualCache類裡面擷取的,這個是Cache接口的基礎實作類,除了實作了Cache對緩存操作的方法外,還提供了一個map集合,來存儲緩存。Cache除了被這個基礎類實作以外,還被很多其它的類實作(有興趣的可以自己去檢視),通過這些類來裝飾PerpetualCache,使其增加功能

CacheKey說明

CacheKey是我們緩存的key,我們來看一下它是由那些組成的,我們來看BaseExecutor的createCacheKey方法

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;
  }
           

從上面的更新參數中可以看出,CacheKey由MappedStatement的id,rowBounds的offset、limit兩個參數,sql語句,查詢參數,Environment的id,共同組成

LocalCacheScope說明

這個類是枚舉類,有兩個參數,分别是SESSION和STATEMENT,從上面的判斷語句我們應該不難看出,當我們配置為STATEMENT的時候,我們執行完sql語句就會立即清除緩存,是以我們在分布式項目當中一定要将這個屬性配置成STATEMENT。

二級緩存

例子

使用二級緩存需要配置兩個地方,一個是全局配置cacheEnabled=true,一個是在mapper裡面配置

Mybatis源碼分析(緩存)介紹一級緩存二級緩存
public static void main(String[] args){
        SqlSession session1 = sqlSessionFactory.openSession(true);
        SqlSession session2 = sqlSessionFactory.openSession(true);
       // session.selectOne("selectCorp");
        UserMapper mapper1 = session1.getMapper(UserMapper.class);
        UserMapper mapper2 = session2.getMapper(UserMapper.class);
        UserDO userDO1 = mapper1.getCorpByCorpId("261");
        //session1.commit();
        UserDO userDO2 = mapper2.getCorpByCorpId("261");
    }
           

我們先把commit這個操作關了看一下列印結果:執行了兩次

Mybatis源碼分析(緩存)介紹一級緩存二級緩存

當我們commit之後,看看列印結果:執行了一次

Mybatis源碼分析(緩存)介紹一級緩存二級緩存

結論

當我們使用二級緩存的時候,即使在不同的sqlSession裡,隻要執行語句相同,我們也能使用緩存,前提是需要送出上一次查詢

底層實作

我們主要來看一下CachingExecutor類下面的query方法,CachingExecutor是一個裝飾類,主要用來裝飾baseExecuor執行器,當我們配置了全局屬性cacheEnabled=true,就會使用CachingExecutor類。

@Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
      //這個cache就是我們在mapper裡面配置的cache,如果不配置就不能用兩級緩存
    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);
  }
           

從上面可以看出來當我們在裡面配置的cache,我們程式就會走二級緩存。

TransactionalCacheManager說明

這個裡面維持着一個map,是以cache為key的,所有說二級緩存是針對mapper層面的緩存

Mybatis源碼分析(緩存)介紹一級緩存二級緩存

TransactionalCache說明

這個就是二級緩存最終存儲資料的地方,想到與一級緩存,是以我們可以知道二級緩存就是同一個mapper裡的一級緩存的集合

Mybatis源碼分析(緩存)介紹一級緩存二級緩存

為什麼我們需要送出,二級緩存才有用

首先我們将我們的緩存存在了TransactionalCache的entriesToAddOnCommit裡

Mybatis源碼分析(緩存)介紹一級緩存二級緩存

而我們取是通過key去SynchronizedCache裡取的

Mybatis源碼分析(緩存)介紹一級緩存二級緩存

是以當我們沒用送出的時候,資料還沒用到SynchronizedCache裡,隻是在entriesToAddOnCommit裡存着,我們再來看看sqlSession commit做了什麼,如圖:最終是将我們儲存在entriesToAddOnCommit裡的資料周遊到PerpetualCache中的map裡

Mybatis源碼分析(緩存)介紹一級緩存二級緩存