Mybatis 緩存實現原理——案例實踐

Mybatis 緩存介紹

關於緩存,想必大家早已不陌生。第一次使用查詢數據時,Mybatis 將其結果緩存起來,當下次執行相同的查詢的時候直接返回(沒有聲明需要刷新緩存,且緩存沒有超時)。

默認情況下,Mybatis 只啓用了本地的會話緩存,它僅僅對一個會話中的數據進行緩存。
一級緩存(本地的會話緩存):只與 SqlSession 有關,不同的 SqlSession 緩存不同。
二級緩存:SqlSession 可以共享緩存。

緩存(總開關)是默認開啓的,如果需要關閉緩存只需在 MyBatis 的配置文件中添加一個屬性設置,
全局性地開啓或關閉所有映射器配置文件中已配置的任何緩存。

  <settings>
    <setting name="cacheEnabled" value="false"/>
  </settings>

開啓二級緩存的方式

要啓用全局的二級緩存,只需要在你的 SQL 映射文件中添加一行:

<cache/> 

還可自定義緩存參數

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

創建了一個 FIFO 緩存,每隔 60 秒刷新,最多可以存儲結果對象或列表的 512 個引用,而且返回的對象被認爲是隻讀的,因此對它們進行修改可能會在不同線程中的調用者產生衝突。

可用的清除策略有(默認的清除策略是 LRU):

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

flushInterval(刷新間隔):屬性可以被設置爲任意的正整數,設置的值應該是一個以毫秒爲單位的合理時間量。 默認情況是不設置,也就是沒有刷新間隔,緩存僅僅會在調用語句時刷新。

size(引用數目):屬性可以被設置爲任意正整數,要注意欲緩存對象的大小和運行環境中可用的內存資源。默認值是 1024。

readOnly(只讀):屬性可以被設置爲 true 或 false。只讀的緩存會給所有調用者返回緩存對象的相同實例。 因此這些對象不能被修改。這就提供了可觀的性能提升。而可讀寫的緩存會(通過序列化)返回緩存對象的拷貝。 速度上會慢一些,但是更安全,因此默認值是 false。

另一種開啓方式,在 Mapper 接口上添加 @CacheNamespace 註解。

@CacheNamespace
public interface AutoConstructorMapper {
}

註解配置和 xml 配置只能選擇一個,否者報錯如下:

Cause: org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. Cause: java.lang.IllegalArgumentException: Caches collection already contains value for org.apache.ibatis

另一種開啓方式,還可和其他命名空間共享相同的緩存配置和實例,在 Mapper 接口上添加 @CacheNamespaceRef註解。

@CacheNamespaceRef(name = "org.apache.ibatis.submitted.cache.PersonMapper") // by name
// 或者 @CacheNamespaceRef(PersonMapper.class) // by type
public interface AutoConstructorMapper {
}

一級緩存的效果(同一個 SqlSession )

默認一級緩存,具體是什麼效果呢?接下來看看下面的例子。

  @Test
  void localCacheTest() {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
      PrimitiveSubject subject1 = mapper.getSubject(1);
      PrimitiveSubject subject2 = mapper.getSubject(1);
      PrimitiveSubject subject3 = mapper.getSubject(1);
      assertTrue(subject1 == subject2);
      assertTrue(subject1 == subject3);
    }
  }

單元測試成功,(subject1 == subject2,且 subject1 == subject3)說明查詢返回是的是相同的對象。
日誌也只打印了一個查詢語句,也說明 SQL 只執行了一次。

2020-05-10 18:07:35 DEBUG [main] - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
2020-05-10 18:07:38 DEBUG [main] - Opening JDBC Connection
2020-05-10 18:07:38 DEBUG [main] - Setting autocommit to false on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@1f760b47]
2020-05-10 18:07:38 DEBUG [main] - ==>  Preparing: SELECT * FROM subject WHERE id = ? 
2020-05-10 18:07:38 DEBUG [main] - ==> Parameters: 1(Integer)
2020-05-10 18:07:38 DEBUG [main] - <==      Total: 1
2020-05-10 18:07:38 DEBUG [main] - Resetting autocommit to true on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@1f760b47]
2020-05-10 18:07:38 DEBUG [main] - Closing JDBC Connection [org.hsqldb.jdbc.JDBCConnection@1f760b47]

一級緩存的效果(不同的 SqlSession )

  @Test
  void localCacheDifferentSqlSession() {
    PrimitiveSubject subject1;
    PrimitiveSubject subject2;
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
      subject1 = mapper.getSubject(1);
    }
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
      subject2 = mapper.getSubject(1);
    }
    assertTrue(subject1 != subject2);
  }

單元測試成功,(subject1 != subject2)說明查詢返回是的是不同的對象。
日誌打印了兩次查詢語句,說明 SQL 執行了兩次,一級緩存對不同的 SqlSession 無效。

2020-05-10 18:07:54 DEBUG [main] - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
2020-05-10 18:07:56 DEBUG [main] - Opening JDBC Connection
2020-05-10 18:07:56 DEBUG [main] - Setting autocommit to false on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@1f760b47]
2020-05-10 18:07:56 DEBUG [main] - ==>  Preparing: SELECT * FROM subject WHERE id = ? 
2020-05-10 18:07:56 DEBUG [main] - ==> Parameters: 1(Integer)
2020-05-10 18:07:56 DEBUG [main] - <==      Total: 1
2020-05-10 18:07:56 DEBUG [main] - Resetting autocommit to true on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@1f760b47]
2020-05-10 18:07:56 DEBUG [main] - Closing JDBC Connection [org.hsqldb.jdbc.JDBCConnection@1f760b47]
2020-05-10 18:07:56 DEBUG [main] - Opening JDBC Connection
2020-05-10 18:07:56 DEBUG [main] - Setting autocommit to false on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@51bd8b5c]
2020-05-10 18:07:56 DEBUG [main] - ==>  Preparing: SELECT * FROM subject WHERE id = ? 
2020-05-10 18:07:56 DEBUG [main] - ==> Parameters: 1(Integer)
2020-05-10 18:07:56 DEBUG [main] - <==      Total: 1
2020-05-10 18:07:56 DEBUG [main] - Resetting autocommit to true on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@51bd8b5c]
2020-05-10 18:07:56 DEBUG [main] - Closing JDBC Connection [org.hsqldb.jdbc.JDBCConnection@51bd8b5c]

二級緩存的效果(自定義緩存 )

接下來,我們測試一下開啓二級緩存,配置如下:

<mapper namespace="org.apache.ibatis.autoconstructor.AutoConstructorMapper">
  <cache
    eviction="FIFO"
    flushInterval="60000"
    size="512"
    readOnly="true"/>
  <select id="getSubject" resultType="org.apache.ibatis.autoconstructor.PrimitiveSubject">
    SELECT * FROM subject WHERE id = #{id}
  </select>
</mapper>

執行會出現什麼結果呢

  @Test
  void twoLevelCacheDifferentSqlSession() {
    PrimitiveSubject subject1;
    PrimitiveSubject subject2;
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
      subject1 = mapper.getSubject(1);
    }
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);
      subject2 = mapper.getSubject(1);
    }
    log.debug(subject1.toString());
    log.debug(subject2.toString());
    subject1.setAge(27);
    log.debug(subject1.toString());
    log.debug(subject2.toString());
    assertTrue(subject1 == subject2);
  }
2020-05-10 20:48:36 DEBUG [main] - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
2020-05-10 20:48:38 DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.autoconstructor.AutoConstructorMapper]: 0.0
2020-05-10 20:48:38 DEBUG [main] - Opening JDBC Connection
2020-05-10 20:48:38 DEBUG [main] - Setting autocommit to false on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@4ea5b703]
2020-05-10 20:48:38 DEBUG [main] - ==>  Preparing: SELECT * FROM subject WHERE id = ? 
2020-05-10 20:48:38 DEBUG [main] - ==> Parameters: 1(Integer)
2020-05-10 20:48:38 DEBUG [main] - <==      Total: 1
2020-05-10 20:48:38 DEBUG [main] - Resetting autocommit to true on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@4ea5b703]
2020-05-10 20:48:38 DEBUG [main] - Closing JDBC Connection [org.hsqldb.jdbc.JDBCConnection@4ea5b703]
2020-05-10 20:48:38 DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.autoconstructor.AutoConstructorMapper]: 0.5
2020-05-10 20:48:38 DEBUG [main] - PrimitiveSubject{id=1, name='a', age=10, height=100, weight=45, active=true, dt=Sun May 10 20:48:38 CST 2020}
2020-05-10 20:48:38 DEBUG [main] - PrimitiveSubject{id=1, name='a', age=10, height=100, weight=45, active=true, dt=Sun May 10 20:48:38 CST 2020}
2020-05-10 20:48:38 DEBUG [main] - PrimitiveSubject{id=1, name='a', age=27, height=100, weight=45, active=true, dt=Sun May 10 20:48:38 CST 2020}
2020-05-10 20:48:38 DEBUG [main] - PrimitiveSubject{id=1, name='a', age=27, height=100, weight=45, active=true, dt=Sun May 10 20:48:38 CST 2020}

SQL 只執行了一次,且多了一行輸出 Cache Hit Ratio [org.apache.ibatis.autoconstructor.AutoConstructorMapper]: 0.5 ,說明二次緩存命中。

這裏需要注意的是 Cache 的配置 ***readOnly=“true”***,如果修改緩存,是會影響其他調用者的。
可以看 subject1.setAge(27) 前後的輸出結果。

直接使用默認配置會如何呢?

獲取緩存報 NotSerializableException 錯?

若使用默認配置 ***<cache/>***,可能會報如下錯誤:

org.apache.ibatis.cache.CacheException: Error serializing object.  Cause: java.io.NotSerializableException: org.apache.ibatis.autoconstructor.PrimitiveSubject

此時需要給 PrimitiveSubject 類實現 Serializable 接口,因爲默認 ***readOnly=“false”***,是通過 SerializedCache 類來實現的,序列化和反序列化需要實現 Serializable 接口,序列化可保證業務更改獲取到的值不影響實際的緩存。具體實現代碼在:

##org.apache.ibatis.mapping.CacheBuilder#setStandardDecorators

      if (readWrite) {
        cache = new SerializedCache(cache);
      }

public class SerializedCache implements Cache {

  public void putObject(Object key, Object object) {
    if (object == null || object instanceof Serializable) {
      delegate.putObject(key, serialize((Serializable) object));
    } else {
      throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object);
    }
  }
  
  public Object getObject(Object key) {
    Object object = delegate.getObject(key);
    return object == null ? null : deserialize((byte[]) object);
  }
}

二級緩存的效果(默認緩存配置 )

<mapper namespace="org.apache.ibatis.autoconstructor.AutoConstructorMapper">
<!--  <cache-->
<!--    eviction="FIFO"-->
<!--    flushInterval="60000"-->
<!--    size="512"-->
<!--    readOnly="true"/>-->
  <cache/>
  <select id="getSubject" resultType="org.apache.ibatis.autoconstructor.PrimitiveSubject">
    SELECT * FROM subject WHERE id = #{id}
  </select>
  @Test
  void twoLevelCacheDifferentSqlSessionWithDefaultCache() {
    ...
    log.debug(subject1.toString());
    log.debug(subject2.toString());
    assertTrue(subject1 != subject2);
  }

單元測試成功,且 subject1 != subject2(不相等,是因爲反序列化後返回的結果),但是兩者的內容是一樣的,SQL 只執行一次,緩存命中。

2020-05-10 20:49:38 DEBUG [main] - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
2020-05-10 20:49:40 DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.autoconstructor.AutoConstructorMapper]: 0.0
2020-05-10 20:49:40 DEBUG [main] - Opening JDBC Connection
2020-05-10 20:49:40 DEBUG [main] - Setting autocommit to false on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@4ea5b703]
2020-05-10 20:49:40 DEBUG [main] - ==>  Preparing: SELECT * FROM subject WHERE id = ? 
2020-05-10 20:49:40 DEBUG [main] - ==> Parameters: 1(Integer)
2020-05-10 20:49:40 DEBUG [main] - <==      Total: 1
2020-05-10 20:49:40 DEBUG [main] - Resetting autocommit to true on JDBC Connection [org.hsqldb.jdbc.JDBCConnection@4ea5b703]
2020-05-10 20:49:40 DEBUG [main] - Closing JDBC Connection [org.hsqldb.jdbc.JDBCConnection@4ea5b703]
2020-05-10 20:49:40 DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.autoconstructor.AutoConstructorMapper]: 0.5
2020-05-10 20:49:40 DEBUG [main] - PrimitiveSubject{id=1, name='a', age=10, height=100, weight=45, active=true, dt=Sun May 10 20:49:40 CST 2020}
2020-05-10 20:49:40 DEBUG [main] - PrimitiveSubject{id=1, name='a', age=10, height=100, weight=45, active=true, dt=Sun May 10 20:49:40 CST 2020}

使用第三方緩存作爲二級緩存

Mybatis 二級緩存默認使用 PerpetualCache 進行本地存儲,不能滿足分佈式系統的要求。
可以通過實現你自己的緩存,來完全覆蓋緩存行爲。

<cache type="com.domain.something.MyCustomCache">
  <property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
</cache>

type 屬性指定的類必須實現 org.apache.ibatis.cache.Cache 接口。

若需對緩存進行配置,只需要簡單地在你的緩存實現中添加公有的 JavaBean 屬性,然後通過 cache 元素傳遞屬性值。

總結

Mybatis 默認只啓用了本地的會話緩存,如果要開啓二級緩存則另外需要增加配置,也可使用自定義的二級緩存實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章