圖解 | 聊聊 MyBatis 快取

2022-10-20 12:00:55

首發公眾號-悟空聊架構:圖解 | 聊聊 MyBatis 快取

你好,我是悟空。

本文主要內容如下:

一、MyBatis 快取中的常用概念

MyBatis 快取:它用來優化 SQL 資料庫查詢的,但是可能會產生髒資料。

SqlSession:代表和資料庫的一次對談,向用戶提供了運算元據庫的方法。

MappedStatement:代表要發往資料庫執行的指令,可以理解為是 SQL 的抽象表示。

Executor: 代表用來和資料庫互動的執行器,接受 MappedStatment 作為引數。

namespace:每個 Mapper 檔案只能設定一個 namespace,用來做 Mapper 檔案級別的快取共用。

對映介面:定義了一個介面,然後裡面的介面方法對應要執行 SQL 的操作,具體要執行的 SQL 語句是寫在對映檔案中。

對映檔案:MyBatis 編寫的 XML 檔案,裡面有一個或多個 SQL 語句,不同的語句用來對映不同的介面方法。通常來說,每一張單表都對應著一個對映檔案。

二、MyBatis 一級快取

2.1 一級快取原理

在一次 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 查詢的結果:

2.2 一級快取設定

在 mybatis-config.xml 檔案設定,name=localCacheScope,value有兩種值:SESSIONSTATEMENT

<configuration>
    <settings>
        <setting name="localCacheScope" value="SESSION"/>
    </settings>
<configuration>

SESSION:開啟一級快取功能

STATEMENT:快取只對當前執行的這一個 SQL 語句有效,也就是沒有用到一級快取功能。

首先我們通過幾個考題來體驗下 MyBatis 一級快取。

2.3 一級快取考題

考題(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。

2.4 MyBatis 一級快取失效的場景

  1. 不同的SqlSession對應不同的一級快取
  2. 同一個SqlSession但是查詢條件不同
  3. 同一個SqlSession兩次查詢期間執行了任何一次增刪改操作
  4. 同一個SqlSession兩次查詢期間手動清空了快取

2.5 MyBatis 一級快取總結

  • MyBatis一級快取內部設計簡單,只是一個沒有容量限定的 HashMap,在快取的功能性上有所欠缺

  • MyBatis的一級快取最大範圍是SqlSession內部,有多個SqlSession或者分散式的環境下,資料庫寫操作會引起髒資料,建議設定快取級別為Statement

  • 一級快取的設定中,預設是 SESSION 級別,即在一個MyBatis對談中執行的所有語句,都會共用這一個快取。

三、MyBatis 二級快取

3.1 MyBatis 二級快取概述

  • MyBatis的二級快取相對於一級快取來說,實現了SqlSession之間快取資料的共用,同時粒度更加的細,能夠到namespace級別,通過Cache介面實現類不同的組合,對Cache的可控性也更強。

  • MyBatis在多表查詢時,極大可能會出現髒資料,有設計上的缺陷,安全使用二級快取的條件比較苛刻。

  • 在分散式環境下,由於預設的MyBatis Cache實現都是基於原生的,分散式環境下必然會出現讀取到髒資料,需要使用集中式快取將 MyBatis的Cache 介面實現,有一定的開發成本,直接使用Redis、Memcached 等分散式快取可能成本更低,安全性也更高。

3.2 MyBatis 二級快取原理

一級快取最大的共用範圍就是一個 SqlSession 內部,如果多個 SqlSession 之間需要共用快取,則需要使用到二級快取。

開啟二級快取後,會使用 CachingExecutor 裝飾 Executor,進入一級快取的查詢流程前,先在CachingExecutor 進行二級快取的查詢。

二級快取開啟後,同一個 namespace下的所有操作語句,都影響著同一個Cache。

每個 Mapper 檔案只能設定一個 namespace,用來做 Mapper 檔案級別的快取共用。

<mapper namespace="mapper.StudentMapper"></mapper>

二級快取被同一個 namespace 下的多個 SqlSession 共用,是一個全域性的變數。MyBatis 的二級快取不適應用於對映檔案中存在多表查詢的情況。

通常我們會為每個單表建立單獨的對映檔案,由於MyBatis的二級快取是基於namespace的,多表查詢語句所在的namspace無法感應到其他namespace中的語句對多表查詢中涉及的表進行的修改,引發髒資料問題。

3.3 MyBatis快取查詢的順序

  • 先查詢二級快取,因為二級快取中可能會有其他程式已經查出來的資料,可以拿來直接使用
  • 如果二級快取沒有命中,再查詢一級快取
  • 如果一級快取也沒有命中,則查詢資料庫
  • SqlSession關閉之後,一級快取中的資料會寫入二級快取。

3.4 二級快取設定

開啟二級快取需要在 mybatis-config.xml 中設定:

<settingname="cacheEnabled"value="true"/>

3.5 二級快取考題

測試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 自定義快取

4.1 MyBatis 自定義快取概述

當 MyBatis 二級快取不能滿足要求時,可以使用自定義快取替換。(較少使用)

自定義快取需要實現 MyBatis 規定的介面:org.apache.ibatis.cache.Cache。這個介面裡面定義了 7 個方法,我們需要自己去實現對應的快取邏輯。

4.2 整合第三方快取 EHCache

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"/>

4.3 EHCache組態檔說明

五、總結

本篇分別介紹了 MyBatis 一級快取、二級快取、自定義快取的原理和使用,其中還穿插了 4 道考題來驗證 MyBatis 快取的功能。不足之處是 MyBatis 快取原始碼未分析。

參考資料:

https://tech.meituan.com/2018/01/19/mybatis-cache.html

關於我

多年網際網路摸爬滾打經驗,擅長微服務、分散式、架構設計。目前在一家大型上市公司從事基礎架構和效能優化工作。

InfoQ 簽約作者、藍橋簽約作者、阿里雲專家博主、51CTO 紅人。

我的所有文章都彙總到這裡了  http://www.passjava.cn