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: