首發公眾號-悟空聊架構:圖解 | 聊聊 MyBatis 快取
你好,我是悟空。
本文主要內容如下:
MyBatis 快取:它用來優化 SQL 資料庫查詢的,但是可能會產生髒資料。
SqlSession:代表和資料庫的一次對談,向用戶提供了運算元據庫的方法。
MappedStatement:代表要發往資料庫執行的指令,可以理解為是 SQL 的抽象表示。
Executor: 代表用來和資料庫互動的執行器,接受 MappedStatment 作為引數。
namespace:每個 Mapper 檔案只能設定一個 namespace,用來做 Mapper 檔案級別的快取共用。
對映介面:定義了一個介面,然後裡面的介面方法對應要執行 SQL 的操作,具體要執行的 SQL 語句是寫在對映檔案中。
對映檔案:MyBatis 編寫的 XML 檔案,裡面有一個或多個 SQL 語句,不同的語句用來對映不同的介面方法。通常來說,每一張單表都對應著一個對映檔案。
在一次 SqlSession 中(資料庫對談),程式執行多次查詢,且查詢條件完全相同,多次查詢之間程式沒有其他增刪改操作,則第二次及後面的查詢可以從快取中獲取資料,避免走資料庫。
每個SqlSession中持有了Executor,每個Executor中有一個LocalCache。當用戶發起查詢時,MyBatis根據當前執行的語句生成MappedStatement
,在Local Cache進行查詢,如果快取命中的話,直接返回結果給使用者,如果快取沒有命中的話,查詢資料庫,結果寫入Local Cache
,最後返回結果給使用者。
Local Cache 其實是一個 hashmap 的結構:
private Map<Object, Object> cache = new HashMap<Object, Object>();
如下圖所示,有兩個 SqlSession,分別為 SqlSession1 和 SqlSession2,每個 SqlSession 中都有自己的快取,快取是 hashmap 結構,存放的鍵值對。
鍵是 SQL 語句組成的 Key :
Statement Id + Offset + Limmit + Sql + Params
值是 SQL 查詢的結果:
在 mybatis-config.xml 檔案設定,name=localCacheScope
,value有兩種值:SESSION
和 STATEMENT
<configuration>
<settings>
<setting name="localCacheScope" value="SESSION"/>
</settings>
<configuration>
SESSION:開啟一級快取功能
STATEMENT:快取只對當前執行的這一個 SQL 語句有效,也就是沒有用到一級快取功能。
首先我們通過幾個考題來體驗下 MyBatis 一級快取。
考題(1)只開啟了一級快取,下面的程式碼呼叫了三次查詢操作 getStudentById,請判斷,下列說法正確的是?
// 開啟一個 SqlSession
SqlSession sqlSession = factory.openSession(true);
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
// 根據 id=1 查詢學生資訊
System.out.println(studentMapper.getStudentById(1));
// 根據 id=1 查詢學生資訊
System.out.println(studentMapper.getStudentById(1));
// 根據 id=1 查詢學生資訊
System.out.println(studentMapper.getStudentById(1));
答案:第一次從資料庫查詢到的資料,第二次和第二次從 MyBatis 一級快取查詢的資料。
解答:第一次從資料庫查詢後,後續查詢走 MyBatis 一級快取
考題(2)只開啟了一級快取,下面程式碼範例中,開啟了一個 SqlSession 對談,呼叫了一次查詢,然後對資料進行了更改,又呼叫了一次查詢,下列關於兩次查詢的說法,正確的是?
// 開啟一個 SqlSession
SqlSession sqlSession = factory.openSession(true);
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
// 根據 id=1 查詢學生資訊
System.out.println(studentMapper.getStudentById(1));
// 插入了一條學生資料,改變了資料庫
System.out.println("增加了" + studentMapper.addStudent(buildStudent()) + "個學生");
// 根據 id=1 查詢學生資訊
System.out.println(studentMapper.getStudentById(1));
sqlSession.close();
答案:第一次從資料庫查詢到的資料,第二次從資料庫查詢的資料
解答:第一次從資料庫查詢後,後續更新(包括增刪改)資料庫中的資料後,這條 SQL 語句的快取失效了,後續查詢需要重新從資料庫獲取資料。
考題(3)當開啟了一級快取,下面的程式碼中,開啟了兩個 SqlSession,第一個 SqlSession 查詢了兩次學生 A 的姓名,第二次 SqlSession 更新了一次學生 A 的姓名,請判斷哪個選項符合最後的查詢結果。
SqlSession sqlSession1 = factory.openSession(true);
SqlSession sqlSession2 = factory.openSession(true);
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class); studentMapper2.updateStudentName("B",1);
System.out.println(studentMapper.getStudentById(1));
System.out.println(studentMapper2.getStudentById(1));
答案:
A
B
解答:只開啟一級快取的情況下,SqlSession 級別是不共用的。程式碼範例中,分別建立了兩個 SqlSession,在第一個 SqlSession 中查詢學生 A 的姓名,第二個 SqlSession 中修改了學生 A 的姓名為 B,SqlSession2 更新了資料後,不會影響 SqlSession1,所以 SqlSession1 查到的資料還是 A。
MyBatis一級快取內部設計簡單,只是一個沒有容量限定的 HashMap,在快取的功能性上有所欠缺
MyBatis的一級快取最大範圍是SqlSession內部,有多個SqlSession或者分散式的環境下,資料庫寫操作會引起髒資料,建議設定快取級別為Statement
一級快取的設定中,預設是 SESSION 級別,即在一個MyBatis對談中執行的所有語句,都會共用這一個快取。
MyBatis的二級快取相對於一級快取來說,實現了SqlSession
之間快取資料的共用,同時粒度更加的細,能夠到namespace
級別,通過Cache介面實現類不同的組合,對Cache的可控性也更強。
MyBatis在多表查詢時,極大可能會出現髒資料,有設計上的缺陷,安全使用二級快取的條件比較苛刻。
在分散式環境下,由於預設的MyBatis Cache實現都是基於原生的,分散式環境下必然會出現讀取到髒資料,需要使用集中式快取將 MyBatis的Cache 介面實現,有一定的開發成本,直接使用Redis、Memcached 等分散式快取可能成本更低,安全性也更高。
一級快取最大的共用範圍就是一個 SqlSession
內部,如果多個 SqlSession
之間需要共用快取,則需要使用到二級快取。
開啟二級快取後,會使用 CachingExecutor
裝飾 Executor
,進入一級快取的查詢流程前,先在CachingExecutor 進行二級快取的查詢。
二級快取開啟後,同一個 namespace
下的所有操作語句,都影響著同一個Cache。
每個 Mapper 檔案只能設定一個 namespace,用來做 Mapper 檔案級別的快取共用。
<mapper namespace="mapper.StudentMapper"></mapper>
二級快取被同一個 namespace
下的多個 SqlSession
共用,是一個全域性的變數。MyBatis 的二級快取不適應用於對映檔案中存在多表查詢的情況。
通常我們會為每個單表建立單獨的對映檔案,由於MyBatis的二級快取是基於namespace
的,多表查詢語句所在的namspace
無法感應到其他namespace
中的語句對多表查詢中涉及的表進行的修改,引發髒資料問題。
開啟二級快取需要在 mybatis-config.xml 中設定:
<settingname="cacheEnabled"value="true"/>
測試update
操作是否會重新整理該namespace
下的二級快取。
開啟了一級和二級快取,通過三個SqlSession 查詢和更新 學生張三的姓名,判斷最後的輸出結果是什麼?
SqlSession sqlSession1 = factory.openSession(true);
SqlSession sqlSession2 = factory.openSession(true);
SqlSession sqlSession3 = factory.openSession(true);
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
StudentMapper studentMapper3 = sqlSession3.getMapper(StudentMapper.class); System.out.println("studentMapper讀取資料: " + studentMapper.getStudentById(1));
sqlSession1.commit();
System.out.println("studentMapper2讀取資料: " + studentMapper2.getStudentById(1)); studentMapper3.updateStudentName("李四",1);
sqlSession3.commit();
System.out.println("studentMapper2讀取資料: " + studentMapper2.getStudentById(1));
答案:
張三
張三
李四
解答:三個 SqlSession 是共用 MyBatis 快取,SqlSession2 更新資料後,MyBatis 的 namespace 快取(StudentMapper) 就失效了,SqlSession2 最後是從資料庫查詢到的資料。
當 MyBatis 二級快取不能滿足要求時,可以使用自定義快取替換。(較少使用)
自定義快取需要實現 MyBatis 規定的介面:org.apache.ibatis.cache.Cache
。這個介面裡面定義了 7 個方法,我們需要自己去實現對應的快取邏輯。
EHCache 和 MyBatis 已經幫我們整合好了一個自定義快取,我們可以直接拿來用,不需要自己去實現 MyBatis 的 org.apache.ibatis.cache.Cache
介面。
新增 mybatis-ehcache 依賴包。
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.2.1</version>
</dependency>
建立EHCache的組態檔ehcache.xml。
<?xml version="1.0" encoding="utf-8" ?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
<!-- 磁碟儲存路徑 -->
<diskStore path="D:\passjava\ehcache"/>
<defaultCache
maxElementsInMemory="1000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
設定二級快取的型別,在xxxMapper.xml檔案中設定二級快取型別
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
本篇分別介紹了 MyBatis 一級快取、二級快取、自定義快取的原理和使用,其中還穿插了 4 道考題來驗證 MyBatis 快取的功能。不足之處是 MyBatis 快取原始碼未分析。
參考資料:
https://tech.meituan.com/2018/01/19/mybatis-cache.html
多年網際網路摸爬滾打經驗,擅長微服務、分散式、架構設計。目前在一家大型上市公司從事基礎架構和效能優化工作。
InfoQ 簽約作者、藍橋簽約作者、阿里雲專家博主、51CTO 紅人。
我的所有文章都彙總到這裡了 http://www.passjava.cn