mybatis快取-二級快取

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 //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對象。