背景:前端
提及 mybatis,做爲 Java 程序員應該是無人不知,它是經常使用的數據庫訪問框架。與 Spring 和 Struts 組成了 Java Web 開發的三劍客--- SSM。固然隨着 Spring Boot 的發展,如今愈來愈多的企業採用的是 SpringBoot + mybatis 的模式開發,咱們公司也不例外。而 mybatis 對於我也僅僅停留在會用而已,沒想過怎麼去了解它,更不知道它的緩存機制了,直到那個生死難忘的 BUG。故事的背景比較長,但並非囉嗦,只是讓讀者知道這個 BUG 觸發的場景,加深記憶。在遇到相似問題時,能夠迅速定位。java
先說下故事的前提,爲了防止用戶在動態中輸入特殊字符,用戶的動態都是編碼後發到後臺,然後臺在存入到 DB 表以前會解碼以方便在 DB 中查看以及上報到搜索引擎。而在查詢用戶動態的時候先從 DB 表中讀取並在後臺作一次編碼再傳到前端,前端再解碼既能夠正常展現了。流程以下圖:程序員
有一天後端預發環境發佈完畢後,用戶的動態頁面有的動態顯示正常,而有的動態倒是被編碼過的。看到現象後的第一個反應就是部分被編碼了兩次,可是編碼操做只會在 service 層的 findById 中有。理論不會在上層犯這種低級錯誤,因而開始排查新增長的代碼。發現只要進入了新增長代碼中的某個 if 分支則被編碼了兩次。分支中除了再次調用 findById(必要性不討論),也無其餘特殊代碼了。百思不得其解後請教了旁邊的老司機,老司機說多是 mybatis 緩存。因而看了下我代碼,將編碼的操做從 findById 中移出來後再次發佈到預發,正常了,心想老司機不愧是老司機。本次 BUG 觸發的有兩個條件須要注意:算法
因而,便開始谷歌 mybatis 的緩存機制,搜到了一篇很是不錯的文章《聊聊 mybatis 的緩存機制》,推薦你們看一下,特別是裏面的流程圖。同時關注下美團技術官方公衆號,上面有不少乾貨(這不是廣告)。可是這篇文章講到了源碼,涉及的比較深。並且並沒講 SpringBoot 下 mybatis 下的一些緩存知識點,遂做此篇,以做補充。sql
緩存的配置數據庫
SpringBoot + mybatis 環境搭建很簡單並且網上一堆教程,這裏不班門弄斧了,記得在項目中將 mytatis 的源碼下載下來便可。mybaits 一共有兩級緩存:一級緩存的配置 key 是 localCacheScope,而二級緩存的配置 key 是 cacheEnabled,從名字上能夠得出如下信息:apache
先來看下在 SpringBoot中 如何配置 mybatis 緩存的相關信息。默認狀況下 SpringBoot 下的 mybatis 一級緩存爲 SESSION 級別,二級緩存也是打開的,能夠在 mybatis 源碼中的 org.apache.ibatis.session.Configuration.class 文件中看到(idea中打開),以下圖:後端
也能夠經過如下測試程序查看緩存開啓狀況緩存
一二級緩存同時開啓的狀況下,數據的查詢順序是 二級緩存 -> 一級緩存 -> 數據庫。一級緩存比較簡單,而二級緩存能夠設置更多的屬性,只須要在 mapper 的 xml 文件中的 <cache /> 配置便可,具體以下:
<cache type = "org.mybatis.caches.ehcache.LoggingEhcache" //指定使用的緩存類,mybatis默認使用HashMap進行緩存,能夠指定第三方緩存 eviction = "LRU" //默認是 LRU 淘汰緩存的算法,有以下幾種: //1.LRU – 最近最少使用的:移除最長時間不被使用的對象。 //2.FIFO – 先進先出:按對象進入緩存的順序來移除它們。 //3.SOFT – 軟引用:移除基於垃圾回收器狀態和軟引用規則的對象。 //4.WEAK – 弱引用:更積極地移除基於垃圾收集器狀態和弱引用規則的對象 flushInterval = "1000" //清空緩存的時間間隔,單位毫秒,能夠被設置爲任意的正整數。 默認狀況是不設置,也就是沒有刷新間隔,緩存僅僅調用語句時刷新。 size = "100" //緩存對象的個數,任意正整數,默認值是1024。 readOnly = "true" //緩存是否只讀,提升讀取效率 blocking = "true" //是否使用阻塞緩存,默認爲false,當指定爲true時將採用BlockingCache進行封裝,blocking, //阻塞的意思,使用BlockingCache會在查詢緩存時鎖住對應的Key,若是緩存命中了則會釋放對應的鎖, //不然會在查詢數據庫之後再釋放鎖這樣能夠阻止併發狀況下多個線程同時查詢數據,詳情可參考BlockingCache的源碼。 />
觸發 mybatis 緩存
(1)配置一級緩存爲 SESSION 級別
Controller 中作兩次調用,代碼以下:
從圖中的 1/2/3/4 能夠看出每次 mapper 層的一次接口調用如 getOne 就會建立一個 session,而且在執行完畢後關閉 session。因此兩次調用並不在一個 session 中,一級緩存並無發生做用。開啓事務,Controller 層代碼以下:
@RequestMapping("/getUser") @Transactional(rollbackFor = Throwable.class) public UserEntity getUser(Long id) { //第一次調用 UserEntity user1=userMapper.getOne(id); //第二次調用 UserEntity user2=userMapper.getOne(id); return user1; }
打印結果以下:
因爲在同一個事務中,雖然調用了 select 操做兩次可是隻執行了一次 sql ,緩存發揮了做用。這就跟一開始我遇到的那個 BUG 場景同樣:同一 session 且 select 調用 > 1 次。若是在兩次調用中間插入 update 操做,緩存會當即失效。只要 session 中有 insert、update 和 delete 語句,該 session 中的緩存會當即被刷新。可是注意這只是在同一 session 之間。不一樣 session 之間如 session1 和 session2,session1 裏的 insert/update/delete 並不會影響 session 2 下的緩存,這在高併發或者分佈式的狀況下會產生髒數據。因此建議將一級緩存級別調成 statement。
(2)配置一級緩存爲 STATEMENT 級別
再次將(1)中的無事務和有事務的代碼分別執行一遍,打印結果始終以下:
配置成 SATEMENT 後,一級緩存至關於被關閉了。STATEMENT 級別暫時很差模擬,可是我猜想 STATEMENT 級別即在同一執行 sql 的接口中(如上面的 getOne 中)緩存,出了 getOne 緩存即失效。
(3)二級緩存,同時爲了不一級緩存的干擾,將一級緩存設置爲 STATEMENT
Controller 中去掉 @Transactional 註解代碼以下:
從圖中紅框能夠看出第二次查詢命中緩存,0.5 是命中率,
再次執行 http://localhost:8080/getUser?id=1 打印結果以下:
此次一次 sql 也沒執行了,因此說二級緩存全局緩存。但它的緩存範圍也是有限的,一級緩存在同一個 session 中。二級緩存能夠跨 session 但也只能在同一 namespace 中,所謂 namespace 即 mapper xml 文件中。具體實驗請看《聊聊 mybatis 的緩存機制》中的關於二級緩存的實驗 4 和 5。再看下二級緩存配置對二級緩存的影響,爲了明顯的看出效果,只改以下配置:
controller 代碼:
http://localhost:8080/getUser?id=1&id2=2 最後打印的結果以下:
太長了,拼接下:
能夠看出二級緩存只能緩存一個對象且 5s 後就失效了,緩存失效。
總結:
我推薦的文章中總結的已經很是好了,直接引用下:
一、MyBatis一級緩存的生命週期和SqlSession一致。
二、MyBatis一級緩存內部設計簡單,只是一個沒有容量限定的HashMap,在緩存的功能性上有所欠缺。
三、MyBatis的一級緩存最大範圍是SqlSession內部,有多個SqlSession或者分佈式的環境下,數據庫寫操做會引發髒數據,建議設定緩存級別爲Statement。
四、MyBatis的二級緩存相對於一級緩存來講,實現了SqlSession之間緩存數據的共享,同時粒度更加的細,可以到namespace級別,經過Cache接口實現類不一樣的組合,對Cache的可控性也更強。
五、MyBatis在多表查詢時,極大可能會出現髒數據,有設計上的缺陷,安全使用二級緩存的條件比較苛刻。
六、在分佈式環境下,因爲默認的MyBatis Cache實現都是基於本地的,分佈式環境下必然會出現讀取到髒數據,須要使用集中式緩存將MyBatis的Cache接口實現,有必定的開發成本,直接使用Redis、Memcached等分佈式緩存可能成本更低,安全性也更高。
7. 我的建議MyBatis緩存特性在生產環境中進行關閉,單純做爲一個ORM框架使用可能更爲合適。