mybatis快取-二級快取

Cn_FallTime發表於2022-03-25

1.2 二級快取

  • 【官方宣告】 => 如何開啟【二級快取】

    預設情況下,只啟用了本地的會話快取,它僅僅對一個會話中的資料進行快取。 要啟用全域性的二級快取,只需要在你的 SQL 對映檔案中新增一行:

    • 在XML對映檔案中新增以下程式碼,以開啟【二級快取】
    <cache/>
  • 【官方宣告】 => 【二級快取】的作用

    • 對映語句檔案中的所有 select 語句的結果將會被快取。
    • 對映語句檔案中的所有 insert、update 和 delete 語句會重新整理快取。
    • 快取會使用最近最少使用演算法(LRU, Least Recently Used)演算法來清除不需要的快取。
    • 快取不會定時進行重新整理(也就是說,沒有重新整理間隔)。
    • 快取會儲存列表或物件(無論查詢方法返回哪種)的 1024 個引用。
    • 快取會被視為讀/寫快取,這意味著獲取到的物件並不是共享的,可以安全地被呼叫者修改,而不干擾其他呼叫者或執行緒所做的潛在修改。
  • 【官方提示】 => 【二級快取】的作用域

    • 快取只作用於 cache 標籤所在的對映檔案中的語句。如果你混合使用 Java API 和 XML 對映檔案,在共用介面中的語句將不會被預設快取。你需要使用 @CacheNamespaceRef 註解指定快取作用域。
  • 【官方宣告】 => <cache>標籤的屬性修改

  <cache
    eviction="FIFO"
    flushInterval="60000"
    size="512"
    readOnly="true"/>

這個更高階的配置建立了一個 FIFO 快取,每隔 60 秒重新整理,最多可以儲存結果物件或列表的 512 個引用,而且返回的物件被認為是隻讀的,因此對它們進行修改可能會在不同執行緒中的呼叫者產生衝突。

可用的清除策略有:

  • LRU – 最近最少使用:移除最長時間不被使用的物件。
  • FIFO – 先進先出:按物件進入快取的順序來移除它們。
  • SOFT – 軟引用:基於垃圾回收器狀態和軟引用規則移除物件。
  • WEAK – 弱引用:更積極地基於垃圾收集器狀態和弱引用規則移除物件。

預設的清除策略是 LRU。

flushInterval(重新整理間隔)屬性可以被設定為任意的正整數,設定的值應該是一個以毫秒為單位的合理時間量。 預設情況是不設定,也就是沒有重新整理間隔,快取僅僅會在呼叫語句時重新整理。

size(引用數目)屬性可以被設定為任意正整數,要注意欲快取物件的大小和執行環境中可用的記憶體資源。預設值是 1024。

readOnly(只讀)屬性可以被設定為 true 或 false。只讀的快取會給所有呼叫者返回快取物件的相同例項。 因此這些物件不能被修改。這就提供了可觀的效能提升。而可讀寫的快取會(通過序列化)返回快取物件的拷貝。 速度上會慢一些,但是更安全,因此預設值是 false。

  • 總結
    image

1.2.1 什麼是二級快取?

MyBatis的二級快取是Application級別的快取,它可以提高對資料庫查詢的效率,以提高應用的效能。

二級快取即使當一級快取被清除/關閉也會存在(即sqlsession.close()方法執行後依舊會儲存查詢快取)

SqlSessionFactory層面上的二級快取預設是不開啟的,二級快取的開啟需要進行配置,實現二級快取的時候,MyBatis要求返回的POJO必須是可序列化的( 要求實現Serializable介面)

1.2.2 二級快取的作用

  • 對映語句檔案中的所有select語句將會被快取。
  • 對映語句檔案中的所有insert、update和delete語句會重新整理快取。
  • 快取會使用預設的Least Recently Used(LRU,最近最少使用的)演算法來收回。
  • 根據時間表,比如No Flush Interval,(CNFI沒有重新整理間隔),快取不會以任何時間順序來重新整理。
  • 快取會儲存列表集合或物件(無論查詢方法返回什麼)的1024個引用
  • 快取會被視為是read/write(可讀/可寫)的快取,意味著物件檢索不是共享的,而且可以安全的被呼叫者修改,不干擾其他呼叫者或執行緒所做的潛在修改。

1.2.3 測試

  1. 實現類 => 【實現Serializable介面】
@Data
//實現二級快取返回的pojo物件必須要求是安全的。
//由於二級快取的資料不一定都是儲存到記憶體中,它的儲存介質多種多樣,所以需要給快取的物件執行序列化。如果儲存在記憶體中的話,實測不序列化也可以的。
public class User implements Serializable {
    private int id;
    private String name;
    private String pwd;
}
  1. XML配置檔案 => 【開啟二級快取】

    • 預設配置
     <cache/>
  • 自定義配置
     <cache
           eviction="FIFO" 
           flushInterval="3000"
           size="512"
           readOnly="true"/>
  1. 測試操作
    @Test
    public void Test03(){
        SqlSession sqlSession01 = MybatisUtils.getSqlSession();
        SqlSession sqlSession02 = MybatisUtils.getSqlSession();
        UserMapper mapper01 = sqlSession01.getMapper(UserMapper.class);
        User user01 = mapper01.queryUserById(2);
        System.out.println(user01.toString());
        sqlSession01.close();
        System.out.println("------------***************----------------");
        UserMapper mapper02 = sqlSession02.getMapper(UserMapper.class);
        User user02 = mapper02.queryUserById(2);
        System.out.println(user02.toString());
        System.out.println(user01==user02);
        sqlSession02.close();
    }

第一次先不開啟二級快取
image

  • 執行結果如下:可以看出再不開啟二級快取的情況下,兩個sqlsession產生的結果物件並不一致,且查詢操作sql語句使用了兩次
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
Opening JDBC Connection
Created connection 2130772866.
==>  Preparing: select * from mybatis.user where id=?
==> Parameters: 2(Integer)
<==    Columns: id, name, pwd
<==        Row: 2, lisi, 123456
<==      Total: 1
User(id=2, name=lisi, pwd=123456)
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7f010382]
Returned connection 2130772866 to pool.
------------***************----------------
Opening JDBC Connection
Checked out connection 2130772866 from pool.
==>  Preparing: select * from mybatis.user where id=?
==> Parameters: 2(Integer)
<==    Columns: id, name, pwd
<==        Row: 2, lisi, 123456
<==      Total: 1
User(id=2, name=lisi, pwd=123456)
false
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7f010382]
Returned connection 2130772866 to pool.

程式已結束,退出程式碼0

第二次開啟二級快取
image

  • 結果如下:可以看出開啟二級快取後查詢結果兩個sqlsession產生的返回物件是同一個,且sql語句只呼叫了一次
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
Cache Hit Ratio [com.zhang.dao.UserMapper]: 0.0
Opening JDBC Connection
Created connection 963110412.
==>  Preparing: select * from mybatis.user where id=?
==> Parameters: 2(Integer)
<==    Columns: id, name, pwd
<==        Row: 2, lisi, 123456
<==      Total: 1
User(id=2, name=lisi, pwd=123456)
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3967e60c]
Returned connection 963110412 to pool.
------------***************----------------
Cache Hit Ratio [com.zhang.dao.UserMapper]: 0.5
User(id=2, name=lisi, pwd=123456)
true

程式已結束,退出程式碼0

注意如果不進行快取只讀設定很可能會重新整理第一次查詢的快取記錄
image

  • 根據下方的測試結果一,可以看出我們不設定readOnly="true"時,返回的user01,user02和user03都是不同的,

二級快取是事務性的。這意味著,當 SqlSession 完成並提交時,或是完成並回滾,但沒有執行 flushCache=true 的 insert/delete/update 語句時,快取會獲得更新。
image

  • 根據下方的測試結果二,可以看出我們設定readOnly="true"時,返回的user01和user02是相同的,但user02和user03是不同的,
       @Test
    public void Test03(){
        SqlSession sqlSession01 = MybatisUtils.getSqlSession();
        SqlSession sqlSession02 = MybatisUtils.getSqlSession();
        UserMapper mapper01 = sqlSession01.getMapper(UserMapper.class);
        User user01 = mapper01.queryUserById(2);
        System.out.println(user01.toString());
        sqlSession01.close();

        System.out.println("------------***************----------------");
        UserMapper mapper02 = sqlSession02.getMapper(UserMapper.class);
        User user02 = mapper02.queryUserById(2);
        System.out.println(user02.toString());
        System.out.println(user01==user02);
        User user = new User();
        user.setId(3);
        user.setPwd("6113081");
        user.setName("FT");
        int i = mapper02.updateUser(user);
        if (i >= 0) {
            System.out.println("更新成功");
            sqlSession02.commit();//事務一旦提交就會重新整理快取
        }else{
            System.out.println("更新失敗");
            sqlSession02.close();
        }
        User user03 = mapper02.queryUserById(2);
        System.out.println(user03==user02);
        sqlSession02.close();
    }
測試結果一
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
Cache Hit Ratio [com.zhang.dao.UserMapper]: 0.0
Opening JDBC Connection
Created connection 2061347276.
==>  Preparing: select * from mybatis.user where id=?
==> Parameters: 2(Integer)
<==    Columns: id, name, pwd
<==        Row: 2, lisi, 123456
<==      Total: 1
User(id=2, name=lisi, pwd=123456)
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7adda9cc]
Returned connection 2061347276 to pool.
------------***************----------------
As you are using functionality that deserializes object streams, it is recommended to define the JEP-290 serial filter. Please refer to https://docs.oracle.com/pls/topic/lookup?ctx=javase15&id=GUID-8296D8E8-2B93-4B9A-856E-0A65AF9B8C66
Cache Hit Ratio [com.zhang.dao.UserMapper]: 0.5
User(id=2, name=lisi, pwd=123456)
false
Opening JDBC Connection
Checked out connection 2061347276 from pool.
==>  Preparing: update mybatis.user set name=?,pwd=? where id=?
==> Parameters: FT(String), 6113081(String), 3(Integer)
<==    Updates: 1
更新成功
Cache Hit Ratio [com.zhang.dao.UserMapper]: 0.3333333333333333
==>  Preparing: select * from mybatis.user where id=?
==> Parameters: 2(Integer)
<==    Columns: id, name, pwd
<==        Row: 2, lisi, 123456
<==      Total: 1
false
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7adda9cc]
Returned connection 2061347276 to pool.

程式已結束,退出程式碼0
測試結果二
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
Cache Hit Ratio [com.zhang.dao.UserMapper]: 0.0
Opening JDBC Connection
Created connection 963110412.
==>  Preparing: select * from mybatis.user where id=?
==> Parameters: 2(Integer)
<==    Columns: id, name, pwd
<==        Row: 2, lisi, 123456
<==      Total: 1
User(id=2, name=lisi, pwd=123456)
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3967e60c]
Returned connection 963110412 to pool.
------------***************----------------
Cache Hit Ratio [com.zhang.dao.UserMapper]: 0.5
User(id=2, name=lisi, pwd=123456)
true
Opening JDBC Connection
Checked out connection 963110412 from pool.
==>  Preparing: update mybatis.user set name=?,pwd=? where id=?
==> Parameters: FT(String), 6113081(String), 3(Integer)
<==    Updates: 1
更新成功
Cache Hit Ratio [com.zhang.dao.UserMapper]: 0.3333333333333333
==>  Preparing: select * from mybatis.user where id=?
==> Parameters: 2(Integer)
<==    Columns: id, name, pwd
<==        Row: 2, lisi, 123456
<==      Total: 1
false
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3967e60c]
Returned connection 963110412 to pool.

程式已結束,退出程式碼0

1.2.4 小結

二級快取是mapper級別的快取,它的實現機制跟一級快取差不多,也是基於PerpetualCache的HashMap本地儲存。作用域為mapper的namespace,可以自定義儲存,比如Ehcache。Mybatis的二級快取是跨Session的,每個Mapper享有同一個二級快取域。
Mybatis內部儲存快取使用一個HashMap,key為hashCode+sqlId+Sql語句。value為從查詢出來對映生成的Java物件。

相關文章