為了增加查詢的性能,mybatis 提供了二級緩存架構(gòu),分為一級緩存和二級緩存。
這兩級緩存最大的區(qū)別就是:一級緩存是會話級別的,只要出了這個 SqlSession,緩存就沒用了。而二級緩存可以跨會話,多個會話可以使用相同的緩存!
一級緩存使用簡單,默認就開啟。二級緩存需要手動開啟,相對復(fù)雜,而且要注意的事項也多,否則可能有隱患。
一級緩存
應(yīng)用場景
訂單表與會員表是存在一對多的關(guān)系,為了盡可能減少join查詢,進行了分階段查詢。即先查詢出訂單表,再根據(jù)member_id
字段查詢出會員表,最后進行數(shù)據(jù)整合。而如果訂單表中存在重復(fù)的member_id
,就會出現(xiàn)很多重復(fù)查詢。
針對這種情況,mybatis通過一級緩存來解決:在同一次查詢會話(SqlSession)中如果出現(xiàn)相同的語句及參數(shù),就會從緩存中取出,不再走數(shù)據(jù)庫查詢。
一級緩存只能作用于查詢會話中,所以也叫做會話緩存。
生效的條件
一級緩存要生效,必須滿足以下條件條件:
- 必須是相同的會話
- 必須是同一個 mapper,即同一個 namespace
- 必須是相同的 statement,即同一個 mapper 中的同一個方法
- 必須是相同的 sql 和參數(shù)
-
查詢語句中間沒有執(zhí)行
session.clearCache()
方法 - 查詢語句中間沒有執(zhí)行 insert/update/delete 方法(無論變動記錄是否與緩存數(shù)據(jù)有無關(guān)系)
與springboot集成時一級緩存不生效原因
因為一級緩存是會話級別的,要生效的話,必須要在同一個 SqlSession 中。但是與 springboot 集成的 mybatis,默認每次執(zhí)行sql語句時,都會創(chuàng)建一個新的 SqlSession!所以一級緩存才沒有生效。
當(dāng)調(diào)用 mapper 的方法時,最終會執(zhí)行到 SqlSessionUtils
的 getSqlSession
方法,在這個方法中會嘗試在事務(wù)管理器中獲取 SqlSession,如果沒有開啟事務(wù),那么就會 new 一個 DefaultSqlSession
。
所以說,即便在同一個方法中,通過同一個 mapper 連續(xù)調(diào)用兩次相同的查詢方法,也不會觸發(fā)一級緩存。
解決與springboot集成時一級緩存不生效問題
在上面的代碼中也看到了,mybatis 在查詢時,會先從事務(wù)管理器中嘗試獲取 SqlSession
,取不到才會去創(chuàng)建新的 SqlSession
。所以可以猜測只要將方法開啟事務(wù),那么一級緩存就會生效。
加上 @Transactional
注解,看下效果:
沒錯,的確生效了。在代碼中可以看到,從事務(wù)管理器中,獲取到了 SqlSession:
再看看源碼中是什么時候?qū)?SqlSession 設(shè)置到事務(wù)管理器中的。
SqlSessionUtils
中,在獲取到 SqlSession
后,會調(diào)用 registerSessionHolder
方法注冊 SessionHolder
到事務(wù)管理器:
具體是在 TransactionSynchronizationManager
的 bindResource
方法中操作的,將 SessionHolder
保存到線程本地變量(ThreadLocal) resources
中,這是每個線程獨享的。
然后在下次查詢時,就可以從這里取出此 SqlSession,使用同一個 SqlSession 查詢,一級緩存就生效了。
所以基本原理就是:如果當(dāng)前線程存在事物,并且存在相關(guān)會話,就從 ThreadLocal 中取出。如果沒有事務(wù),就重新創(chuàng)建一個 SqlSession 并存儲到 ThreadLocal 當(dāng)中,共下次查詢使用。
至于緩存查詢數(shù)據(jù)的地方,是在 BaseExecutor
中的 queryFromDatabase
方法中。執(zhí)行 doQuery 從數(shù)據(jù)庫中查詢數(shù)據(jù)后,會立馬緩存到 localCache(PerpetualCache類型)
中:
基于 Spring Boot + MyBatis Plus + Vue & Element 實現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
- 項目地址:https://github.com/YunaiV/ruoyi-vue-pro
- 視頻教程:https://doc.iocoder.cn/video/
二級緩存
應(yīng)用場景
業(yè)務(wù)系統(tǒng)中存在很多的靜態(tài)數(shù)據(jù)如,字典表、菜單表、權(quán)限表等,這些數(shù)據(jù)的特性是不會輕易修改但又是查詢的熱點數(shù)據(jù)。
一級緩存針對的是同一個會話當(dāng)中相同SQL,并不適合這情熱點數(shù)據(jù)的緩存場景。
為了解決這個問題引入了二級緩存,它脫離于會話之外,多個會話可以使用相同的緩存。
看一個例子:
@RestController
@RequestMapping("item")
publicclassItemController{
@Autowired
privateItemMapperitemMapper;
@GetMapping("/{id}")
publicvoidgetById(@PathVariable("id")Longid){
System.out.println("====================begin====================");
Itemitem=itemMapper.selectById(id);
System.out.println(JSON.toJSONString(item));
}
}
當(dāng)發(fā)送兩次 get 請求時(兩個不同的會話),通過日志可以發(fā)現(xiàn)第二次查詢使用的是緩存
開啟的方法
二級緩存需要手動來開啟,mybatis 默認沒有開啟二級緩存。
1)在 yaml 中配置 cache-enabled
為 true
mybatis-plus:
configuration:
cache-enabled:true
2)Mapper 接口上添加 @CacheNamespace
注解
3)實體類實現(xiàn) Serializable
接口
生效的條件
- 當(dāng)會話提交或關(guān)閉之后才會填充二級緩存
- 必須是同一個 mapper,即同一個命名空間
- 必須是相同的 statement,即同一個 mapper 中的同一個方法
- 必須是相同的 SQL 語句和參數(shù)
-
如果
readWrite=true
(默認就是true),實體對像必須實現(xiàn)Serializable
接口
緩存清除條件
- 只有修改會話提交之后,才會執(zhí)行清空操作
-
xml 中配置的 update 不能清空
@CacheNamespace
中的緩存數(shù)據(jù) -
任何一種增刪改操作都會清空整個
namespace
中的緩存
源碼中是如何填充二級緩存的?
在生效條件中提到了,二級緩存必須要在會話提交或關(guān)閉之后,才能生效!
在查詢到結(jié)果后,會調(diào)用 SqlSession 的 commit 方法進行提交(如果開啟事務(wù)的話,提交 SqlSession 走的不是這里了,但最終填充二級緩存的地方是一樣的。):
在此方法中,最終會調(diào)用到 TransactionalCache
的 flushPendingEntries
方法中填充二級緩存:
springboot 集成 mybatis 的話,如果沒有開啟事務(wù),每次執(zhí)行查詢,都會創(chuàng)建新的 SqlSession,所以即使是在同一個方法中進行查詢操作,那也是跨會話的。
查詢時如何使用二級緩存?
在查詢的時候,最終會調(diào)用 MybatisCachingExecutor
的 query 方法,里面會從 TransactionalCacheManager
中嘗試根據(jù) key 獲取二級緩存的內(nèi)容。
可以看到,這個 key 很長,由 mapper、調(diào)用的查詢方法、SQL 等信息拼接而成,這也是為什么想要二級緩存生效,必須滿足前面所說的條件。
如果能在二級緩存中查詢到,就直接返回了,不需要訪問數(shù)據(jù)庫。
具體的調(diào)用層數(shù)實在太多,用到了裝飾者模式,最終是在 PerpetualCache
中獲取緩存的:
打印日志是在 LoggingCache
中:
為什么mybatis默認不開啟二級緩存?
答案就是,不推薦使用二級緩存!
二級緩存雖然能帶來一定的好處,但是有很大的隱藏危害!
它的緩存是以 namespace(mapper)
為單位的,不同 namespace 下的操作互不影響。且 insert/update/delete 操作會清空所在 namespace
下的全部緩存。
那么問題就出來了,假設(shè)現(xiàn)在有 ItemMapper
以及 XxxMapper
,在 XxxMapper
中做了表關(guān)聯(lián)查詢,且做了二級緩存。此時在 ItemMapper
中將 item 信息給刪了,由于不同 namespace 下的操作互不影響,XxxMapper
的二級緩存不會變,那之后再次通過 XxxMapper
查詢的數(shù)據(jù)就不對了,非常危險。
來看一個例子:
@Mapper
@Repository
@CacheNamespace
publicinterfaceXxxMapper{
@Select("selecti.iditemId,i.nameitemName,p.amount,p.unit_priceunitPrice"+
"fromitemiJOINpaymentponi.id=p.item_idwherei.id=#{id}")
ListgetPaymentVO(Longid) ;
}
@Autowired
privateXxxMapperxxxMapper;
@Test
voidtest(){
System.out.println("====================查詢PaymentVO====================");
ListvoList=xxxMapper.getPaymentVO(1L);
System.out.println(JSON.toJSONString(voList.get(0)));
System.out.println("====================更新item表的name====================");
Itemitem=itemMapper.selectById(1);
item.setName("java并發(fā)編程");
itemMapper.updateById(item);
System.out.println("====================重新查詢PaymentVO====================");
ListvoList2=xxxMapper.getPaymentVO(1L);
System.out.println(JSON.toJSONString(voList2.get(0)));
}
上面的代碼,test()
方法中前后兩次調(diào)用了 xxxMapper.getPaymentVO
方法,因為沒有加 @Transactional
注解,所以前后兩次查詢,是兩個不同的會話,第一次查詢完后,SqlSession
會自動 commit,所以二級緩存能夠生效;
然后在中間進行了 Item 表的更新操作,修改了下名稱;
由于 itemMapper
與 xxxMapper
不是同一個命名空間,所以 itemMapper
執(zhí)行的更新操作不會影響到 xxxMapper
的二級緩存;
再次調(diào)用 xxxMapper.getPaymentVO
,發(fā)現(xiàn)取出的值是走緩存的,itemName
還是老的。但實際上 itemName
在上面已經(jīng)被改了!
執(zhí)行日志如下:
所以說,二級緩存的隱藏危害是比較大的,當(dāng)有表關(guān)聯(lián)時,一個不注意就會出問題,不建議使用。
-
緩存
+關(guān)注
關(guān)注
1文章
240瀏覽量
26697 -
管理器
+關(guān)注
關(guān)注
0文章
246瀏覽量
18540 -
mybatis
+關(guān)注
關(guān)注
0文章
61瀏覽量
6716
原文標(biāo)題:為什么不推薦使用 MyBatis 二級緩存
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論