Mybatis 快取原理

Mybatis 快取原理

本文來自拉鉤 java 高薪訓練營,如果文章寫的不好,看不懂可以找我要課程影片,不收費。

  • 只願在編程道路上,尋求志同道合的碼友。v:15774135883

1 Mybatis 快取機制

Mybatis 提供了一級、二級快取。

  • 一級快取:執行緒級別的快取,也稱為本地快取sqlSession級別的快取,一級快取是默認存在的,同一個會話中,查詢兩次相同的操作就會從快取中取。
  • 二級快取:全局範圍的快取;除了當前sqlSession能用外,其他的也可以使用。二級快取默認也是開啟的,只需要在 mapper 文件中寫一個即可實現,二級快取的實現需要 pojo 實現序列化的介面,否則會出錯

搭建工程

快速搭建一個項目,以便更加深入的了解原理。

  • 創建一個普通 maven 項目即可。(就不做演示了)
  • 依賴中的 mysql 版本換成你們資料庫對應的版本,我的是 8.0

img

    <dependencies>
        <!--mybatis坐標-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.5</version>
        </dependency>
        <!--mysql驅動坐標-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.25</version>
            <scope>runtime</scope>
        </dependency>
        <!--單元測試坐標-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.7</version>
        </dependency>

        <!-- Mybatis整合redis做快取 -->
        <dependency>
            <groupId>org.mybatis.caches</groupId>
            <artifactId>mybatis-redis</artifactId>
            <version>1.0.0-beta2</version>
        </dependency>

        <!-- 分頁助手 -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>3.7.5</version>
        </dependency>
        <dependency>
            <groupId>com.github.jsqlparser</groupId>
            <artifactId>jsqlparser</artifactId>
            <version>0.9.1</version>
        </dependency>

        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper</artifactId>
            <version>3.1.2</version>
        </dependency>
    </dependencies>

實體類

@Table(name = "user")
public class User implements Serializable {

    @Id //對應的是註解id
    @GeneratedValue(strategy = GenerationType.IDENTITY) //設置主鍵的生成策略
    private Integer id;

    private String username;
    // get set 省略
    }

mapper


public interface IUserMapper {

    //更新用戶
    @Update("update user set username = #{username} where id = #{id}")
    public void updateUser(User user);


    //根據id查詢用戶
    @Options(useCache = true)
    @Select({"select * from user where id = #{id}"})
    public User findUserById(Integer id);

}

配置文件 sqlMapConfig.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "//mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>

    <!--給實體類的全限定類名給別名-->
    <typeAliases>
        <!--給單獨的實體起別名-->
      <!--  <typeAlias type="com.lagou.pojo.User" alias="user"></typeAlias>-->
        <!--批量起別名:該包下所有的類的本身的類名:別名還不區分大小寫-->
        <package name="com.lagou.pojo"/>
    </typeAliases>

    <!--environments:運行環境-->
    <environments default="development">
        <environment id="development">
            <!--當前事務交由JDBC進行管理-->
            <transactionManager type="JDBC"></transactionManager>
            <!--當前使用mybatis提供的連接池-->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql:///2022_xx_mybatis"/>
                <property name="username" value="root"/>
                <property name="password" value="317311"/>
            </dataSource>
        </environment>
    </environments>

    <!--引入映射配置文件-->
    <mappers>
       <!-- <mapper class="com.lagou.mapper.IUserMapper"></mapper>-->
        <package name="com.lagou.mapper"/>
    </mappers>

</configuration>

**輸出日誌: **log4j.properties

### direct log messages to stdout ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n

### direct messages to file mylog.log ###
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=c:/mylog.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n

### set log levels - for more verbose logging change 'info' to 'debug' ###

log4j.rootLogger=debug, stdout

測試類:

public class CacheTest {

    private IUserMapper userMapper;
    private SqlSession sqlSession;
    private SqlSessionFactory sqlSessionFactory;

    @Before
    public void before() throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        sqlSession = sqlSessionFactory.openSession();
        userMapper = sqlSession.getMapper(IUserMapper.class);
    }

    @Test
    public void test1(){
       // 第一次查詢id為1的用戶 發出sql 查詢的結果,存入快取中
        User user1 = userMapper.findUserById(1);
        System.out.println(user1);
         //第⼆次查詢,由於是同⼀個sqlSession,會在快取中查詢結果
	//如果有,則直接從快取中取出來,不和資料庫進⾏交互
        User user2 = userMapper.findUserById(1);
        System.out.println(user2);
        System.out.println(user1==user2);
    }
}

查看控制台列印情況:

img

  • 同樣是對 user 表進⾏兩次查詢,只不過兩次查詢之間進⾏了⼀次 update 操作。
   @Test
    public void test2() {
        //第⼀次查詢,發出sql語句,並將查詢的結果放⼊快取中
        User u1 = userMapper.findUserById(1);
        System.out.println(u1);
        //第⼆步進⾏了⼀次更新操作,sqlSession.commit()
        User user = new User();
        user.setId(1);
        user.setUsername("tom");
        userMapper.updateUser(user);
        //第⼆次查詢,由於是同⼀個sqlSession.commit(),會清空快取資訊
        //則此次查詢也會發出sql語句
        User u2 = userMapper.findUserById(1);
        System.out.println(u2);
        sqlSession.close();
    }

查看控制台列印情況:

img

總結

  • 第⼀次發起查詢⽤戶 id 為 1 的⽤戶資訊,先去找快取中是否有 id 為 1 的⽤戶資訊,如果沒有,從 資料庫查詢⽤戶資訊。得到⽤戶資訊,將⽤戶資訊存儲到⼀級快取中。
  • 如果中間sqlSession去執⾏commit操作(執⾏插⼊、更新、刪除),則會清空SqlSession 中的 ⼀ 級快取,這樣做的⽬的為了讓快取中存儲的是最新的資訊,避免臟讀
  • 第⼆次發起查詢⽤戶 id 為 1 的⽤戶資訊,先去找快取中是否有 id 為 1 的⽤戶資訊,快取中有,直 接從 快取中獲取⽤戶資訊

img

一級快取源碼探究

⼀級快取到底是什麼?⼀級快取什麼時候被創建、⼀級快取的⼯作流程是怎樣的?帶著如下問題來探究

  • ⼤家可以這樣想,上⾯我們⼀直提到⼀級快取,那麼提到⼀級快取就繞不開 SqlSession,所以索性我們 就直接從 SqlSession,看看有沒有創建快取或者與快取有關的屬性或者⽅法

img

查看⼀圈,發現上述所有⽅法中,好像只有clearCache()和快取沾點關係,那麼就直接從這個⽅法⼊⼿吧,分析源碼時,我們要看它(此類)是誰,它的⽗類和⼦類分別⼜是誰,對如上關係了解了,你才會對這個類有更深的認識,分析了⼀圈,你可能會得到如下這個流程圖

img

  • 找到clearCache()

img

img

img

img

也就是說,快取其實就是 本地存放的⼀個 map 對象,每⼀個 SqISession 都會存放⼀個 map 對象的引⽤,那麼這個 cache 是何 時創建的呢?

img

你覺得最有可能創建快取的地⽅是哪⾥呢?我覺得是Executor,為什麼這麼認為?因為 Executor 是 執⾏器,⽤來執⾏ SQL 請求,⽽且清除快取的⽅法也在Executor中執⾏,所以很可能快取的創建也很 有可 能在 Executor 中,看了⼀圈發現 Executor 中有⼀個 createCacheKey ⽅法,這個⽅法很像是創 建快取的 ⽅法啊,跟進去看看,你發現 createCacheKey ⽅法是由 BaseExecutor 執⾏的,程式碼如下

    CacheKey cacheKey = new CacheKey();
	//MappedStatement 的 id
	// id就是Sql語句的所在位置包名+類名+ SQL名稱
    cacheKey.update(ms.getId());
	//  offset 就是 0
    cacheKey.update(rowBounds.getOffset());
	// limit 就是 Integer.MAXVALUE
    cacheKey.update(rowBounds.getLimit());
	//具體的SQL語句
    cacheKey.update(boundSql.getSql());
	//後⾯是update 了 sql中帶的參數
    cacheKey.update(value);
...
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());

創建快取 key 會經過⼀系列的 update ⽅法,udate ⽅法由⼀個 CacheKey 這個對象來執⾏的,這個 update ⽅法最終由 updateList 的 list 來把五個值存進去,對照上⾯的程式碼和下⾯的圖示,你應該能 理解 這五個值都是什麼了?

img

這⾥需要注意⼀下最後⼀個值,configuration.getEnvironment().getId()這是什麼,這其實就是 定義在 mybatis-config.xml 中的標籤,⻅如下。

img

那麼我們回歸正題,那麼創建完快取之後該⽤在何處呢?總不會憑空創建⼀個快取不使⽤吧?絕對不會 的,經過我們對⼀級快取的探究之後,我們發現⼀級快取更多是⽤於查詢操作,畢竟⼀級快取也叫做查 詢快取吧,為什麼叫查詢快取我們⼀會⼉說。我們先來看⼀下這個快取到底⽤在哪了,我們跟蹤到BaseExecutor的 query ⽅法如下:

img

2 ⼆級快取

⼆級快取的原理和⼀級快取原理⼀樣,第⼀次查詢,會將數據放⼊快取中,然後第⼆次查詢則會直接去 快取中取。但是⼀級快取是基於 sqlSession 的,⽽⼆級快取是基於 mapper ⽂件的 namespace 的,也 就 是說多個 sqlSession 可以共享⼀個 mapper 中的⼆級快取區域,並且如果兩個 mapper 的 namespace 相 同,即使是兩個 mapper,那麼這兩個 mapper 中執⾏ sql 查詢到的數據也將存在相同的⼆級快取區域 中

img

如何使⽤⼆級快取

開啟⼆級快取 和⼀級快取默認開啟不⼀樣,⼆級快取需要我們⼿動開啟 ⾸先在全局配置⽂件 sqlMapConfig.xml ⽂件中加⼊如下程式碼:

    <!--開啟二級快取  -->
    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>

其次在 UserMapper.xml ⽂件中開啟 快取

<!--開啟⼆級快取-->
<cache></cache>

像我們所使用的是註解開發,沒有 mapper.xml 文件,可以用註解實現開啟二級快取

img

開啟了⼆級快取後,還需要將要快取的pojo實現Serializable接⼝,為了將快取數據取出執⾏反序列化操作,因為⼆級快取數據存儲介質多種多樣,不⼀定只存在記憶體中,有可能存在硬碟中,如果我們要再取 這個快取的話,就需要反序列化了。所以 mybatis 中的pojo都去實現Serializable接⼝

測試

⼀、測試⼆級快取和 sqlSession ⽆關

    @Test
    public void SecondLevelCache() {
        //根據 sqlSessionFactory 產⽣ session
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();

        IUserMapper mapper1 = sqlSession1.getMapper(IUserMapper.class);
        IUserMapper mapper2 = sqlSession2.getMapper(IUserMapper.class);
        //第⼀次查詢,發出sql語句,並將查詢的結果放⼊快取中
        User user1 = mapper1.findUserById(1);
        sqlSession1.close(); //清空一級快取

        //第⼆次查詢,即使sqlSession1已經關閉了,這次查詢依然不發出sql語句
        User user2 = mapper2.findUserById(1);
        System.out.println(user1 == user2);
    }

可以看出上⾯兩個不同的 sqlSession,第⼀個關閉了,第⼆次查詢依然不發出 sql 查詢語句

⼆、測試執⾏ commit()操作,⼆級快取數據清空

    @Test
    public void SecondLevelCache() {
        //根據 sqlSessionFactory 產⽣ session
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        SqlSession sqlSession3 = sqlSessionFactory.openSession();

        IUserMapper mapper1 = sqlSession1.getMapper(IUserMapper.class);
        IUserMapper mapper2 = sqlSession2.getMapper(IUserMapper.class);
        IUserMapper mapper3 = sqlSession3.getMapper(IUserMapper.class);
        //第⼀次查詢,發出sql語句,並將查詢的結果放⼊快取中
        User user1 = mapper1.findUserById(1);
        sqlSession1.close(); //清空一級快取

        User user2 = mapper2.findUserById(1);
        //執⾏更新操作,commit()
        User user = new User();
        user.setId(1);
        user.setUsername("lisi");
        mapper3.updateUser(user);
        sqlSession3.commit();

        //第⼆次查詢,由於上次更新操作,快取數據已經清空(防⽌數據臟讀),這⾥必須再次發出sql語
        System.out.println(user1 == user2);

    }

查看控制台情況:
img

useCache 和 flushCache

  • userCache: 禁⽤⼆級快取,直接從數 據 庫中獲取
  • flushCache=”true」屬性,默認情況下為 true,即刷新快取,如果改成 false 則 不 會刷新。使⽤快取時如果⼿動修改資料庫表中的查詢數據會出現臟讀。(一般不設置)

3 ⼆級快取整合 redis

  • 主要解決 分散式快取

上⾯我們介紹了 mybatis ⾃帶的⼆級快取,但是這個快取是單伺服器⼯作,⽆法實現分散式快取。 那麼 什麼是分散式快取呢?假設現在有兩個伺服器 1 和 2,⽤戶訪問的時候訪問了 1 伺服器,查詢後的緩 存就 會放在 1 伺服器上,假設現在有個⽤戶訪問的是 2 伺服器,那麼他在 2 伺服器上就⽆法獲取剛剛那個 緩 存,如下圖所示:

img

為了解決這個問題,就得找⼀個分散式的快取,專⻔⽤來存儲快取數據的,這樣不同的伺服器要快取數 據都往它那⾥存,取快取數據也從它那⾥取,如下圖所示

img

如上圖所示,在⼏個不同的伺服器之間,我們使⽤第三⽅快取框架,將快取都放在這個第三⽅框架中, 然後⽆論有多少台伺服器,我們都能從快取中獲取數據。 這⾥我們介紹mybatis與redis的整合。 剛剛提到過,mybatis提供了⼀個cache接⼝,如果要實現⾃⼰的快取邏輯,實現cache接⼝開發即可。 mybatis 本身默認實現了⼀個,但是這個快取的實現⽆法實現分散式快取,所以我們要⾃⼰來實現。 redis 分散式快取就可以,mybatis提供了⼀個針對cache接⼝的redis實現類,該類存在mybatis-redis包 中 實現

添加依賴:

<dependency>
 <groupId>org.mybatis.caches</groupId>
 <artifactId>mybatis-redis</artifactId>
 <version>1.0.0-beta2</version>
</dependency>

修改二級快取實現 二級快取

  • 下面是 xml 的方式
  • 但是我們文章是基於註解的方式,在 mapper 中修改
<cache type="org.mybatis.caches.redis.RedisCache" />

img

redis.properties

redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=0

img

測試:

執行一個沒有清空二級快取的操作,比如兩次查詢

  • 可以看到 已經存儲到redis中了。

imgimg

源碼分析:

RedisCache和⼤家普遍實現Mybatis的快取⽅案⼤同⼩異,⽆⾮是實現Cache接⼝,並使⽤jedis操作緩 存;不過該項⽬在設計細節上有⼀些區別;

img

RedisCache在mybatis啟動的時候,由MyBatis的CacheBuilder創建,創建的⽅式很簡單,就是調⽤ RedisCache的帶有String參數的構造⽅法,即RedisCache(String id);⽽在RedisCache的構造⽅法中, 調⽤了 RedisConfigu rationBuilder 來創建 RedisConfig 對象,並使⽤ RedisConfig 來創建JedisPool。 RedisConfig類繼承了 JedisPoolConfig,並提供了 host,port等屬性的包裝,簡單看⼀下RedisConfig的 屬性

img

RedisConfig對象是由RedisConfigurationBuilder創建的,簡單看下這個類的主要⽅法:

  • 核⼼的⽅法就是parseConfiguration⽅法,該⽅法從classpath中讀取⼀個redis.properties⽂件:

img

接下來看看Cache中最重要的兩個⽅法:putObject和getObject,通過這兩個⽅法來查看mybatis-redis 儲存數據的格式:

img

可以很清楚的看到,mybatis-redis在存儲數據的時候,是使⽤的hash結構,把cache的id作為這個hash 的key (cache的id在mybatis中就是mapper的namespace);這個mapper中的查詢快取數據作為 hash 的field,需要快取的內容直接使⽤SerializeUtil存儲,SerializeUtil和其他的序列化類差不多,負責 對象 的序列化和反序列化;

Tags: