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