二級緩存介紹
在上文中提到的一級緩存中,其最大的共享范圍就是一個SqlSession內部,如果多個SqlSession之間需要共享緩存,則需要使用到二級緩存。開啟二級緩存后,會使用CachingExecutor裝飾Executor,進入一級緩存的查詢流程前,先在CachingExecutor進行二級緩存的查詢,具體的工作流程如下所示。

二級緩存開啟后,同一個namespace下的所有操作語句,都影響着同一個Cache,即二級緩存被多個SqlSession共享,是一個全局的變量。
當開啟緩存后,數據的查詢執行的流程就是 二級緩存 -> 一級緩存 -> 數據庫。
二級緩存配置
要正確的使用二級緩存,需完成如下配置的。
首先,在MyBatis的配置文件中開啟二級緩存。
<setting name="cacheEnabled" value="true"/>
然后,在MyBatis的 Mapper XML 中加入 cache 或者 cache-ref 標簽。
cache標簽用於聲明這個namespace需要使用二級緩存,並且可以自定義配置。
<cache/>
type:cache使用的類型,默認是PerpetualCache,這在一級緩存中提到過。eviction: 定義回收的策略,常見的有FIFO,LRU。flushInterval: 配置一定時間自動刷新緩存,單位是毫秒。size: 最多緩存對象的個數。readOnly: 是否只讀,若配置可讀寫,則需要對應的實體類能夠序列化。blocking: 若緩存中找不到對應的key,是否會一直blocking,直到有對應的數據進入緩存。
cache-ref代表引用別的命名空間的Cache配置,兩個命名空間的操作使用的是同一個Cache。
<cache-ref namespace="mapper.StudentMapper"/>
二級緩存實驗
public void testCacheWithoutCommitOrClose() throws Exception {
SqlSession sqlSession1 = factory.openSession(false); // 不自動提交事務
SqlSession sqlSession2 = factory.openSession(false); // 不自動提交事務
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
System.out.println(studentMapper.getStudentById(1)); // 查詢數據庫
System.out.println(studentMapper2.getStudentById(1)); // 查詢數據庫,二級緩存不起作用
}
public void testCacheWithCommitOrClose() throws Exception {
SqlSession sqlSession1 = factory.openSession(false); // 不自動提交事務
SqlSession sqlSession2 = factory.openSession(false); // 不自動提交事務
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
System.out.println(studentMapper.getStudentById(1)); // 查詢數據庫
sqlSession1.commit(); // 提交事務
System.out.println(studentMapper2.getStudentById(1)); // 查詢緩存,二級緩存起作用
}
public void testCacheWithUpdate() throws Exception {
SqlSession sqlSession1 = factory.openSession(false); // 不自動提交事務
SqlSession sqlSession2 = factory.openSession(false); // 不自動提交事務
SqlSession sqlSession3 = factory.openSession(false); // 不自動提交事務
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
StudentMapper studentMapper3 = sqlSession3.getMapper(StudentMapper.class);
System.out.println(studentMapper.getStudentById(1)); // 查詢數據庫
sqlSession1.commit(); // 提交事務
System.out.println(studentMapper2.getStudentById(1)); // 查詢緩存,二級緩存起作用
studentMapper3.updateStudentName("方方",1); // 更新數據
sqlSession3.commit(); // 提交事務
System.out.println(studentMapper2.getStudentById(1)); // 查詢數據庫,說明清除了二級緩存
}
public void testCacheWithDiffererntNamespace() throws Exception {
SqlSession sqlSession1 = factory.openSession(false); // 不自動提交事務
SqlSession sqlSession2 = factory.openSession(false); // 不自動提交事務
SqlSession sqlSession3 = factory.openSession(false); // 不自動提交事務
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
ClassMapper classMapper = sqlSession3.getMapper(ClassMapper.class);
System.out.println(studentMapper.getStudentByIdWithClassInfo(1)); // 關聯查詢數據庫
sqlSession1.close();
System.out.println(studentMapper2.getStudentByIdWithClassInfo(1)); // 查詢緩存,二級緩存起作用
classMapper.updateClassName("特色一班",1); // 更新其中一個表的數據
sqlSession3.commit(); // 提交更新事務
System.out.println(studentMapper2.getStudentByIdWithClassInfo(1)); // 仍然查詢緩存,引起了臟讀
}
// 讓ClassMapper引用StudenMapper的命名空間,這樣兩個映射文件對應的SQL操作都會使用同一塊緩存
public void testCacheWithDiffererntNamespace() throws Exception {
SqlSession sqlSession1 = factory.openSession(false); // 不自動提交事務
SqlSession sqlSession2 = factory.openSession(false); // 不自動提交事務
SqlSession sqlSession3 = factory.openSession(false); // 不自動提交事務
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
ClassMapper classMapper = sqlSession3.getMapper(ClassMapper.class);
System.out.println(studentMapper.getStudentByIdWithClassInfo(1)); // 關聯查詢數據庫
sqlSession1.close();
System.out.println(studentMapper2.getStudentByIdWithClassInfo(1)); // 查詢緩存,二級緩存起作用
classMapper.updateClassName("特色一班",1); // 更新其中一個表的數據
sqlSession3.commit(); // 提交更新事務
System.out.println(studentMapper2.getStudentByIdWithClassInfo(1)); // 關聯查詢數據庫,二級緩存失效
}
總結
- MyBatis的二級緩存相對於一級緩存來說,實現了
SqlSession之間緩存數據的共享,同時粒度更加的細,能夠到namespace級別,通過Cache接口實現類不同的組合,對Cache的可控性也更強。 - MyBatis在多表查詢時,極大可能會出現臟數據,有設計上的缺陷,安全使用二級緩存的條件比較苛刻。
- 在分布式環境下,由於默認的MyBatis Cache實現都是基於本地的,分布式環境下必然會出現讀取到臟數據,需要使用集中式緩存將MyBatis的Cache接口實現,有一定的開發成本,直接使用Redis、Memcached等分布式緩存可能成本更低,安全性也更高。
參考:https://mybatis.org/mybatis-3/zh/configuration.html#settings
http://mybatis.org/spring/zh/factorybean.html
https://tech.meituan.com/2018/01/19/mybatis-cache.html
