mybatis 原始碼分析(四)一二級快取分析
阿新 • • 發佈:2019-08-26
本篇部落格主要講了 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 分散式二級快取的時候,要確保快取淘汰等策略是可以用於分散式快取的;