天天看點

圖解 | 聊聊 MyBatis 緩存

你好,我是悟空。

本文主要内容如下:

圖解 | 聊聊 MyBatis 緩存

一、MyBatis 緩存中的常用概念

MyBatis 緩存:它用來優化 SQL 資料庫查詢的,但是可能會産生髒資料。

SqlSession:代表和資料庫的一次會話,向使用者提供了操作資料庫的方法。

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

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

namespace:每個 Mapper 檔案隻能配置一個 namespace,用來做 Mapper 檔案級别的緩存共享。

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

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

二、MyBatis 一級緩存

2.1 一級緩存原理

在一次 SqlSession 中(資料庫會話),程式執行多次查詢,且查詢條件完全相同,多次查詢之間程式沒有其他增删改操作,則第二次及後面的查詢可以從緩存中擷取資料,避免走資料庫。

圖解 | 聊聊 MyBatis 緩存

每個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 緩存

2.2 一級緩存配置

在 mybatis-config.xml 檔案配置,​

​name=localCacheScope​

​​,value有兩種值:​

​SESSION​

​​ 和 ​

​STATEMENT​

<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。

圖解 | 聊聊 MyBatis 緩存

每個 Mapper 檔案隻能配置一個 namespace,用來做 Mapper 檔案級别的緩存共享。

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

二級緩存被同一個 ​

​namespace​

​​ 下的多個 ​

​SqlSession​

​ 共享,是一個全局的變量。MyBatis 的二級緩存不适應用于映射檔案中存在多表查詢的情況。

通常我們會為每個單表建立單獨的映射檔案,由于MyBatis的二級緩存是基于​

​namespace​

​​的,多表查詢語句所在的​

​namspace​

​​無法感應到其他​

​namespace​

​中的語句對多表查詢中涉及的表進行的修改,引發髒資料問題。

3.3 MyBatis緩存查詢的順序

圖解 | 聊聊 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 個方法,我們需要自己去實作對應的緩存邏輯。

圖解 | 聊聊 MyBatis 緩存

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 緩存

五、總結