mybatis 源碼分析(四)一二級緩存分析


本篇博客主要講了 mybatis 一二級緩存的構成,以及一些容易出錯地方的示例分析;

一、mybatis 緩存體系

mybatis 的一二級緩存體系大致如下:

  • 首先當一二級緩存同時開啟的時候,首先命中二級緩存;
  • 一級緩存位於 BaseExecutor 中不能關閉,但是可以指定范圍 STATEMENT、SESSION;
  • 整個二級緩存雖然經過了很多事務相關的組件,但是最終是落地在 MapperStatement 的 Cache 中(Cache 的具體實例類型可以在 mapper xml 的 cache type 標簽中指定,默認 PerpetualCache),而 MapperStatement 和 namespace 一一對應,所以二級緩存的作用域是 mapper namespace;
  • 在使用二級緩存的時候,如果 cache 沒有命中則向后查找,然后查詢的結果不是直接放到 cache 中,而是首先放到 TransactionCache 的本地緩存中,這里區分 entriesToAddOnCommit、entriesMissedInCache 是為了統計命令率,最后在 sqlSession commit 的時候,才會將 TransactionCache 的本地緩存提交到 cache 中,此時 cache 才是對其他 sqlSession 可見的;
  • 此外當需要分布式緩存的時候,就需要將二級緩存放到 JVM 之外,這里可以實現 cache 接口編寫自己的 cache,此時在實現的 cache 中就可以使用 ehcache、redis 等外部緩存進行操作;

以上就大致是 mybatis 緩存的整體結構,下面將分模塊拆分測試一二級緩存;

二、一級緩存

mybatis 的一級緩存一般情況很少使用,其原因主要有兩個:

  • 一級緩存的生命周期同 SqlSession,所以容易出現臟讀;
  • 一級緩存的 cache 的實現只能是 PerpetualCache,所以不能指定容量等設置;

1. 臟讀測試

指定一級緩存范圍為 SESSION:

<setting name="localCacheScope" value="SESSION"/>
@Test
public void test01() {
  SqlSessionFactory sqlSessionFactory = DBUtils.getSessionFactory();
  try (
    SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
    SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
  ) {
    UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
    UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
    log.info("---get: {}", userMapper1.getUser(1L));
    log.info("---get: {}", userMapper2.getUser(1L));
    log.info("---update: {}", userMapper1.setNameById(1L, "LiSi"));
    log.info("---get: {}", userMapper1.getUser(1L));
    log.info("---get: {}", userMapper2.getUser(1L));
  }
}

結果如下:

[DEBUG] sanzao.db.UserMapper.getUser - ==>  Preparing: select * from user where id = ? 
[DEBUG] sanzao.db.UserMapper.getUser - ==> Parameters: 1(Long)
[TRACE] sanzao.db.UserMapper.getUser - <==    Columns: id, username, password, address
[TRACE] sanzao.db.UserMapper.getUser - <==        Row: 1, ZhangSan, 123456, TT
[DEBUG] sanzao.db.UserMapper.getUser - <==      Total: 1
[INFO] sanzao.Test01 - ---get: User{id=1, user_name='ZhangSan', password='123456', address='TT'}
[DEBUG] org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
[DEBUG] org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 61073295.
[DEBUG] sanzao.db.UserMapper.getUser - ==>  Preparing: select * from user where id = ? 
[DEBUG] sanzao.db.UserMapper.getUser - ==> Parameters: 1(Long)
[TRACE] sanzao.db.UserMapper.getUser - <==    Columns: id, username, password, address
[TRACE] sanzao.db.UserMapper.getUser - <==        Row: 1, ZhangSan, 123456, TT
[DEBUG] sanzao.db.UserMapper.getUser - <==      Total: 1
[INFO] sanzao.Test01 - ---get: User{id=1, user_name='ZhangSan', password='123456', address='TT'}
[DEBUG] sanzao.db.UserMapper.setNameById - ==>  Preparing: update user set username = ? where id = ? 
[DEBUG] sanzao.db.UserMapper.setNameById - ==> Parameters: LiSi(String), 1(Long)
[DEBUG] sanzao.db.UserMapper.setNameById - <==    Updates: 1
[INFO] sanzao.Test01 - ---update: 1
[DEBUG] sanzao.db.UserMapper.getUser - ==> Parameters: 1(Long)
[TRACE] sanzao.db.UserMapper.getUser - <==    Columns: id, username, password, address
[TRACE] sanzao.db.UserMapper.getUser - <==        Row: 1, LiSi, 123456, TT
[DEBUG] sanzao.db.UserMapper.getUser - <==      Total: 1
[INFO] sanzao.Test01 - ---get: User{id=1, user_name='LiSi', password='123456', address='TT'}
[INFO] sanzao.Test01 - ---get: User{id=1, user_name='ZhangSan', password='123456', address='TT'}

可以看到當 sqlSession1 更新的時候,sqlSession2 的緩存仍然有效所以出現了臟讀;所以通常都設置一級緩存的范圍為:STATEMENT;

2. 源碼分析

mybatis 的一級緩存主要和 Executor 整合比較多,所以建議先查看我上一篇博客 Executor 詳解 ,詳細了解緩存命中的整體流程;這里一級緩存的源碼也很簡單:

  • 查詢的時候,首先查緩存,命中則返回,未命中就查數據庫,然后填充緩存;
  • 更新、提交等操作情況緩存;
@SuppressWarnings("unchecked")
@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."); }
  
  // 查詢的時候一般不清楚緩存,但是可以通過 xml配置或者注解強制清除,queryStack == 0 是為了防止遞歸調用
  if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); }
  List<E> list;
  try {
    queryStack++;
    // 首先查看一級緩存
    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();
    }
    deferredLoads.clear();
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
  	  // 一級緩存本身不能關閉,但是可以設置作用范圍 STATEMENT,每次都清除緩存
      clearLocalCache();
    }
  }
  return list;
}

三、二級緩存

mybatis 二級緩存要稍微復雜一點,中間多了一步事務緩存:

  • 首先無論是查詢還是更新,都會按要求清空緩存 flushCacheIfRequired,默認更新清空,查詢不清空,也可以在 xml 或者注解中指定;
  • 查詢的時候,先查緩存,命中返回,未命中查一級緩存、數據庫,然后回填事務緩存,注意這里不是直接填充到緩存中;此時的事務緩存對任何的 SqlSession 都是不可見的,因為自己查詢的時候也是直接查詢的目標緩存;
  • 更新就直接委托給目標 Executor 執行;
  • 最后 SqlSession 執行commit 的時候,將事務緩存刷新到目標緩存中;

1. 事務緩存測試

設置二級緩存:

<setting name="cacheEnabled" value="true"/>

<mapper namespace="***">
  <cache/>
</mapper>
@Test
public void test02() {
  SqlSessionFactory sqlSessionFactory = DBUtils.getSessionFactory();
  try (
    SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
    SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
  ) {
    UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
    UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);

    User u1 = userMapper1.getUser(1L);
    System.out.println("---get u1: " + u1);

    User u2 = userMapper2.getUser(1L);
    System.out.println("---get u2: " + u2);

    User u3 = userMapper1.getUser(1L);
    System.out.println("---get u3: " + u3);
  }
}

打印:

DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
DEBUG [main] - ==>  Preparing: select * from user where id = ? 
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <==      Total: 1
---get u1: User{id=1, user_name='sanzao', password='123456', address='TT'}
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 1613095350.
DEBUG [main] - ==>  Preparing: select * from user where id = ? 
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <==      Total: 1
---get u2: User{id=1, user_name='sanzao', password='123456', address='TT'}
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
---get u3: User{id=1, user_name='sanzao', password='123456', address='TT'}

可以看到:

  • SqlSession1 為提交事務緩存,所以 SqlSession2 又從數據庫中查了一次;
  • 當SqlSession1 再次查詢的時候,二級緩存未命中 Cache Hit Ratio 為 0,但是命中了一級緩存,所以並未再查數據庫;

2. 二級緩存測試

這次我們提交緩存看看是否命中:

@Test
public void test03() {
  SqlSessionFactory sqlSessionFactory = DBUtils.getSessionFactory();
  try (
    SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
    SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
  ) {
    UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
    UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);

    User u1 = userMapper1.getUser(1L);
    System.out.println("---get u1: " + u1);
    sqlSession1.commit();

    User u2 = userMapper2.getUser(1L);
    System.out.println("---get u2: " + u2);

    int i = userMapper1.setNameById(1L, "LiSi");
    System.out.println("---update user: " + i);
    sqlSession1.commit();

    User u3 = userMapper1.getUser(1L);
    System.out.println("---get u3: " + u3);
    sqlSession1.commit();

    User u4 = userMapper2.getUser(1L);
    System.out.println("---get u4: " + u4);
  }
}

打印:

DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
DEBUG [main] - ==>  Preparing: select * from user where id = ? 
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <==      Total: 1
---get u1: User{id=1, user_name='sanzao', password='123456', address='TT'}
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.5
---get u2: User{id=1, user_name='sanzao', password='123456', address='TT'}
DEBUG [main] - ==>  Preparing: update user set username = ? where id = ? 
DEBUG [main] - ==> Parameters: LiSi(String), 1(Long)
DEBUG [main] - <==    Updates: 1
---update user: 1
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.3333333333333333
DEBUG [main] - ==>  Preparing: select * from user where id = ? 
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <==      Total: 1
---get u3: User{id=1, user_name='LiSi', password='123456', address='TT'}
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.5
---get u4: User{id=1, user_name='LiSi', password='123456', address='TT'}

這次就能看到當 SqlSession1 提交事務緩存后,SqlSession2 就能看到了;

3. 緩存配置測試

此外還可以配置各種二級緩存策略,比如大小,刷新間隔時間,淘汰策略等,這里主要就是使用了 Cache 接口的裝飾者模式:

  • LRU – 最近最少使用:移除最長時間不被使用的對象。
  • FIFO – 先進先出:按對象進入緩存的順序來移除它們。
  • SOFT – 軟引用:基於垃圾回收器狀態和軟引用規則移除對象。
  • WEAK – 弱引用:更積極地基於垃圾收集器狀態和弱引用規則移除對象。

但是需要注意的是這里的策略也能用戶本地緩存,對於分布式緩存有些策略還是有問題;比如:

<cache eviction="FIFO" flushInterval="60000" size="2" readOnly="true"/>

這里主要定義了緩存大小2,使用 FIFO 策略更新;

@Test
public void test04() {
  SqlSessionFactory sqlSessionFactory = DBUtils.getSessionFactory();
  try (
    SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
    SqlSession sqlSession2 = sqlSessionFactory.openSession(true);) {
    UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
    UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);

    System.out.println("---get user: " + userMapper1.getUser(1L));
    sqlSession1.commit();

    System.out.println("---get user: " + userMapper1.getUser(2L));
    sqlSession1.commit();

    System.out.println("---get user: " + userMapper1.getUser(3L));
    sqlSession1.commit();

    System.out.println("---get user: " + userMapper2.getUser(1L));

    System.out.println("---get user: " + userMapper2.getUser(2L));

    System.out.println("---get user: " + userMapper1.getUser(1L));
    sqlSession2.commit();

    System.out.println("------------");
    System.out.println("---get user: " + userMapper1.getUser(1L));
    System.out.println("---get user: " + userMapper1.getUser(2L));
    System.out.println("---get user: " + userMapper1.getUser(3L));
  }
}

打印:

DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
DEBUG [main] - ==>  Preparing: select * from user where id = ? 
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <==      Total: 1
---get user: User{id=1, user_name='s1', password='123456', address='TT'}
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
DEBUG [main] - ==>  Preparing: select * from user where id = ? 
DEBUG [main] - ==> Parameters: 2(Long)
DEBUG [main] - <==      Total: 1
---get user: User{id=2, user_name='s2', password='123456', address='TT'}
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
DEBUG [main] - ==>  Preparing: select * from user where id = ? 
DEBUG [main] - ==> Parameters: 3(Long)
DEBUG [main] - <==      Total: 1
---get user: User{id=3, user_name='s3', password='123456', address='TT'}
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
DEBUG [main] - ==>  Preparing: select * from user where id = ? 
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <==      Total: 1
---get user: User{id=1, user_name='s1', password='123456', address='TT'}
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.2
---get user: User{id=2, user_name='s2', password='123456', address='TT'}
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.16666666666666666
DEBUG [main] - ==>  Preparing: select * from user where id = ? 
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <==      Total: 1
---get user: User{id=1, user_name='s1', password='123456', address='TT'}
------------
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.2857142857142857
---get user: User{id=1, user_name='s1', password='123456', address='TT'}
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.25
DEBUG [main] - ==> Parameters: 2(Long)
DEBUG [main] - <==      Total: 1
---get user: User{id=2, user_name='s2', password='123456', address='TT'}
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.3333333333333333
---get user: User{id=3, user_name='s3', password='123456', address='TT'}

從日志中可以看到對於 SqlSession1,大小2,FIFO 是生效的,但是 SqlSession2 提交了之后,就發現緩存 s1,s2,s3 都命中了;

至於源碼太多了就不一次分析了,對於上面說的使用裝飾者模式,可以在 CacheBuilder 中看到;

public Cache build() {
  setDefaultImplementations();
  Cache cache = newBaseCacheInstance(implementation, id);
  setCacheProperties(cache);
  // issue #352, do not apply decorators to custom caches
  if (PerpetualCache.class.equals(cache.getClass())) {
    for (Class<? extends Cache> decorator : decorators) {
      cache = newCacheDecoratorInstance(decorator, cache);
      setCacheProperties(cache);
    }
    cache = setStandardDecorators(cache);
  } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
    cache = new LoggingCache(cache);
  }
  return cache;
}

總結

  • mybatis 一級緩存的生命周期和 SqlSession 是一樣的,通常情況下不建議使用一級緩存,通常將一級緩存范圍設置為 STATEMENT;
  • 使用 mybatis 二級的時候,務必記得 SqlSession.commit ,否則二級緩存是不生效的;
  • 在配置 mybatis 分布式二級緩存的時候,要確保緩存淘汰等策略是可以用於分布式緩存的;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM