本文主題:
- Executor執(zhí)行體系回顧
- 為什么要有一級緩存?
- 一級緩存、二級緩存有什么區(qū)別?
- 一級緩存屬于通用邏輯,那么結(jié)構(gòu)上它是如何設(shè)計的?
- 一級緩存是用什么實現(xiàn)的?
- 一級緩存命中條件有哪些?
- 一級緩存有哪些清空場景?
- Spring和Mybatis整合一級緩存失效?
- 一級緩存的注意事項
Executor執(zhí)行體系
Executor執(zhí)行體系.png
這是從SqlSession到Executor實現(xiàn)的執(zhí)行體系圖,從圖中可以看出,一級緩存是在抽象類BaseExecutor實現(xiàn)的
為什么要有一級緩存?
框架自身方面
一級緩存能夠幫助Mybatis解決【結(jié)果集映射】的循環(huán)引用。假設(shè)張三有一張銀行卡,那么就會形成一種對象結(jié)構(gòu),用戶持有銀行卡,銀行卡歸屬于張三,這就是一種循環(huán)引用(或者理解為互相引用),mybatis在查詢用戶的時候查詢到了張三,發(fā)現(xiàn)張三持有銀行卡,則觸發(fā)了對張三銀行卡的查詢;查到了張三的銀行卡數(shù)據(jù)后,發(fā)現(xiàn)銀行卡有一個歸屬者信息,那又需要查詢用戶表,查到了張三,再查銀行卡,再查張三。。。。。
Mybatis為了解決這種循環(huán)應(yīng)用的結(jié)果映射,借助了一級緩存。具體的細節(jié),會在后面的結(jié)果集解析中再去分析,這里不再贅述
業(yè)務(wù)代碼方面
在程序員生涯中,大家一定遇到過類似的代碼。
public void test(){
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
Long[] ids = {1,1,10,5,10};
for(Long id : ids){
//查詢用戶 如果沒有mybatis一級緩存,那么 id=1、id =10分別都會查詢兩次數(shù)據(jù)庫
User user = userMapper.selectById(id);
//執(zhí)行針對該user的某業(yè)務(wù)邏輯
this.doSomeThing(user);
}
}
上述代碼中,id=1、id=10其實沒有必要查詢兩次數(shù)據(jù)庫,這時候如果自己來解決,那可能需要這樣來做
public void test(){
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
Integer[] ids = {1,1,10,5,10};
Map<Integer,User> userMap = new HashMap<Integer,User>();
for(Integer id : ids){
User user;
if(userMap.containsKey(id)) {
user = userMap.get(id);
}else{
//查詢用戶
user = userMapper.selectById(id);
userMap.put(id,user);
}
//執(zhí)行針對該user的某業(yè)務(wù)邏輯
this.doSomeThing(user);
}
}
Mybatis的一級緩存,就可以解決這個場景的問題,即便第一種場景的代碼,也不用擔心id=1和id=10查詢了兩次的數(shù)據(jù)庫
一級緩存、二級緩存有什么區(qū)別?
一級緩存:會話級緩存,生命周期是整個會話SqlSession,非常短暫,不能直接關(guān)閉,不能跨線程使用
二級緩存:應(yīng)用級緩存,生命周期是整個應(yīng)用,可以跨線程使用
一級緩存屬于通用邏輯,那么結(jié)構(gòu)上它是如何設(shè)計的?
在Mybatis中,sql是由Executor來執(zhí)行的,Executor有3個實現(xiàn)類,如果在每一個實現(xiàn)類中都寫一遍這樣的代碼那就太過冗余了,因此抽象出了一個BaseExecutor類,用來處理一級緩存的相關(guān)邏輯
我們都是調(diào)用query、update兩個方法,query會調(diào)用子類實現(xiàn)doQuery,update會調(diào)用子類實現(xiàn)doUpdate
一級緩存是用什么實現(xiàn)的?
可以看到一級緩存是使用PerpetualCache實現(xiàn)的,而PerpetualCache內(nèi)部的真正實現(xiàn),就是一個HashMap。而PerpetualCache實現(xiàn)Cache接口,Cache接口的定義也非常簡單,核心接口就是 放入、獲取、 移除和清空緩存
一級緩存命中條件有哪些?
從上圖的createCacheKey方法,我們可以看到一級緩存CacheKey的組成,因此可以推斷出一級緩存的命中條件:
1、同一個SqlSession會話。不是同一個會話,就不是同一個localCache,這點很重要!??!
2、StatementId相同 。com.test.UserMapper.selectById
3、分頁參數(shù)RowBounds相同。limit 1
4、SQL語句相同。select id from user where id = ?
5、SQL查詢參數(shù)相同。 id =1
6、環(huán)境相同。environmentId=development 通常不會跨環(huán)境開發(fā),可以忽略
一級緩存有哪些清空場景?
BaseExecutor中有一個clearLocalCache方法用于清理一級緩存,那么找到調(diào)用這個方法的地方,也就找到了清空一級緩存的場景。如上圖所示,可以發(fā)現(xiàn)修改、查詢、提交、回滾,都可能會清空一級緩存,下面具體來看一下每一個場景是如何清空的
查詢:
@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.");
}
//mapper.xml的sql塊上配置了flushCache=true,前置清空
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 {
//從數(shù)據(jù)庫中查詢,查詢到結(jié)果放入localCache
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
//mapper.xml的sql塊上配置了一級緩存作用域statementType="STATEMENT",后置清空
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache();
}
}
return list;
}
增刪改操作update:
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//執(zhí)行sql前清空一級緩存
clearLocalCache();
return doUpdate(ms, parameter);
}
提交:
@Override
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
//提交前清空
clearLocalCache();
flushStatements();
if (required) {
transaction.commit();
}
}
回滾:
@Override
public void rollback(boolean required) throws SQLException {
if (!closed) {
try {
//回滾前清空
clearLocalCache();
flushStatements(true);
} finally {
if (required) {
transaction.rollback();
}
}
}
}
清空一級緩存場景總結(jié):
1、執(zhí)行查詢時的清空,若配置了flushCache="true" ->前置清空; 若配置了statementType="STATEMENT" ->后置清空
2、執(zhí)行增刪改(update)操作
3、提交會話
4、回滾會話
Spring和Mybatis整合一級緩存失效?
Spring和Mybatis整合后,很多人發(fā)現(xiàn)一級緩存不能命中,這是因為Spring是通過SqlSessionTemplate保證了每次sql調(diào)用都會重新生成一個SqlSession會話,而一級緩存是會話級緩存,會話都不同了,自然不能命中。那么如何才能命中一級緩存呢?加一個事務(wù)注解@Transactional就可以了,因為在同一個事務(wù)方法中,如果Spring還去使用不同的SqlSession會話,那么就無法保證事務(wù)原子性。
一級緩存的注意事項
一級緩存的清空是清理掉會話中的全部緩存,它底層是調(diào)用了PerpetualCache的clear方法,也就是HashMap.clear。因此不存在指定sql清空的場景,比如會話中執(zhí)行了查詢用戶、查詢角色兩個sql,我們無法只清空一級緩存中的用戶數(shù)據(jù),只能全部清空。