MyBatis特緩存性詳解
緩存簡介
一般我們在系統中使用緩存技術是為了提升數據查詢的效率。當我們從數據庫中查詢到一批數據後將其放入到混存中(簡單理解就是一塊內存區域),下次再查詢相同數據的時候就直接從緩存中獲取數據就行了。這樣少了一步和數據庫的交互,可以提升查詢的效率。
但是一個硬幣都具有兩面性,緩存在帶來性能提升的同時也「悄悄」引入了很多問題,比如緩存同步、緩存失效、緩存雪崩等等。當然這些問題不是本文討論的重點。
本文主要討論MyBatis緩存這個比較雞肋的功能。雖然說MyBatis的緩存功能比較雞肋,但是為了全面了解MyBatis這個框架,學習下緩存這個功能還是挺有必要的。MyBatis的緩存分為一級緩存和二級緩存,下面就分別來介紹下這兩個特性。
一級緩存
在應用運行過程中,我們有可能在一次數據庫會話中,執行多次查詢條件完全相同的SQL,MyBatis提供了一級緩存的方案優化這部分場景,如果是相同的SQL語句,會優先命中一級緩存,避免直接對數據庫進行查詢,提高性能。
什麼是MyBatis一級緩存
一級緩存是 SqlSession級別 的緩存。在操作數據庫時需要構造 sqlSession 對象,在對象中有一個(內存區域)數據結構(HashMap)用於存儲緩存數據。不同的 sqlSession 之間的緩存數據區域(HashMap)是互相不影響的。
在應用運行過程中,我們有可能在一次數據庫會話中,執行多次查詢條件完全相同的SQL,MyBatis 提供了一級緩存的方案優化這部分場景,如果是相同的SQL語句,會優先命中一級緩存,避免直接對數據庫進行查詢,提高性能。
怎麼開啟一級緩存
MyBatis中一級緩存默認是開啟的,不需要我們做額外的操作。
如果你需要關閉一級緩存的話,可以在Mapper映射文件中將flushCache屬性設置為true,這種做法只會針對單個SQL操作生效
<select id="selectByPrimaryKey" parameterType="java.lang.String" resultMap="BaseResultMap" flushCache="true">
select
<include refid="Base_Column_List" />
from cbondissuer
where OBJECT_ID = #{objectId,jdbcType=VARCHAR}
</select>
> 還有一種做法是在MyBatis的主配置文件中,關閉所有的一級緩存
> ```xml
> 默認是SESSION,也就是開啟一級緩存
> <setting name="localCacheScope" value="STATEMENT"/>
> ```
下面我們來寫代碼驗證下MyBatis的一級緩存。
```java
String id = "123";
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
//同一個sqlSession創建的Mapper
CbondissuerMapper cbondissuerMapper10 = sqlSession1.getMapper(CbondissuerMapper.class);
CbondissuerMapper cbondissuerMapper11 = sqlSession1.getMapper(CbondissuerMapper.class);
//另外一個sqlSession創建的Mapper
CbondissuerMapper cbondissuerMapper20 = sqlSession2.getMapper(CbondissuerMapper.class);
//同一個Mapper,同樣的SQL查了兩次
Cbondissuer cbondissuer10 = cbondissuerMapper10.selectByPrimaryKey(id);
Cbondissuer cbondissuer101 = cbondissuerMapper10.selectByPrimaryKey(id);
//同一個sqlSession創建的Mapper,又查詢了一次同樣的SQL
Cbondissuer cbondissuer11 = cbondissuerMapper11.selectByPrimaryKey(id);
//不一樣的sqlSession創建的Mapper查詢了一次同樣的SQL
Cbondissuer cbondissuer20 = cbondissuerMapper20.selectByPrimaryKey(id);
System.out.println("cbondissuer10 equals cbondissuer101 :"+(cbondissuer10==cbondissuer101));
System.out.println("cbondissuer10 equals cbondissuer11 :"+(cbondissuer10==cbondissuer11));
System.out.println("cbondissuer10 equals cbondissuer21 :"+(cbondissuer10==cbondissuer20));
sqlSession1.close();
sqlSession2.close();
System.out.println("end...");
上面進行了四次查詢,如果你觀察日誌的話。會發現只進行了兩個數據庫查詢。因為第二和第三次的查詢都查詢了一級緩存,查出的其實是緩存中的結果。所以輸出的結果是
cbondissuer10 equals cbondissuer101 :true
cbondissuer10 equals cbondissuer11 :true
cbondissuer10 equals cbondissuer21 :false
哪些因素會使一級緩存失效
上面的一級緩存初探讓我們感受到了 MyBatis 中一級緩存的存在,那麼現在你或許就會有疑問了,那麼什麼時候緩存失效呢?
- 通過同一個SqlSession執行更新操作時,這個更新操作不僅僅指代update操作,還指插入和刪除操作;
- 事務提交時會刪除一級緩存;
- 事務回滾時也會刪除一級緩存;
一級緩存源碼解析
其實MyBatis一級緩存的實質就是一個Executor的一個類似Map的屬性,分析源碼的方法就是看在哪些地方從這個Map中查詢了緩存,又是在哪些清空了這些緩存。
1. 查詢時使用緩存分析
public abstract class BaseExecutor implements Executor {
private static final Log log = LogFactory.getLog(BaseExecutor.class);
protected Transaction transaction;
protected Executor wrapper;
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
//這個localCache變量就是一級緩存變量
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;
//..省略下面代碼
}
全局搜索代碼中哪些地方使用了這個變量,很容易找到BaseExecutor.query方法使用了這個緩存:
public abstract class BaseExecutor implements Executor {
// 省略其他代碼
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
//先從緩存中查詢結果,如果緩存中已經存在結果直接使用緩存的結果
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//緩存中沒有結果從數據庫查詢
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
//..省略下面代碼
}
上面的代碼展示了,BaseExecutor的query方法使用緩存的過程。需要注意的是查詢緩存時是根據cacheKey進行查詢的,我們可以將這個key簡單的
理解為sql語句,不同的sql語句能查出不同的緩存。(注意sql語句中的參數不同也會被認為是不同的sql語句)。
2. 導致一級緩存失效的代碼分析
查看BaseExecutor的代碼,我們很容易發現是下面的方法清空了一級緩存。(不要問我是怎麼發現這個代碼的,看代碼能力需要自己慢慢提升)
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
那麼我們只要查看哪些地方調用了這個方法就知道哪些情況下會導致一級緩存失效了。跟蹤下來,最後發現下面三處地方會使得一級緩存失效
BaseExecutor的update方法,使用MyBatis的接口進行增、刪、改操作都會調用到這個方法,這個也印證了上面的說法。
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}
BaseExecutor的commit方法,事務提交會導致一級緩存失敗。如果我們使用Spring的話,一般事務都是自動提交的,所以好像MyBatis的一級緩存一直沒怎麼被考慮過
@Override
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
clearLocalCache();
flushStatements();
if (required) {
transaction.commit();
}
}
BaseExecutor的rollback方法,事務回滾也會導致一級緩存失效。
@Override
public void rollback(boolean required) throws SQLException {
if (!closed) {
try {
clearLocalCache();
flushStatements(true);
} finally {
if (required) {
transaction.rollback();
}
}
}
}
一級緩存使用建議
平時使用MyBatis時都是和Spring結合使用的,在整個Spring容器中一般只有一個SqlSession實現類。而Spring一般都是主動提交事務的,所以說一級緩存經常失效。
還有就是我們也很少在一個事務範圍內執行同一個SQL兩遍,上面的這些原因導致我們在開發過程中很少注意到MyBatis一級緩存的存在。
不怎麼用並不是說不用,作為一個合格的開發者需要對這些心知肚明,要清楚的知道MyBatis一級緩存的工作流程。
二級緩存
什麼是MyBatis二級緩存
MyBatis 一級緩存最大的共享範圍就是一個SqlSession內部,那麼如果多個 SqlSession 需要共享緩存,則需要開啟二級緩存,開啟二級緩存後,會使用 CachingExecutor 裝飾 Executor,
進入一級緩存的查詢流程前,先在CachingExecutor 進行二級緩存的查詢,具體的工作流程如下所示:
當二級緩存開啟後,同一個命名空間(namespace) 所有的操作語句,都影響着一個 共同的 cache(一個Mapper映射文件對應一個Cache),也就是二級緩存被多個 SqlSession 共享,是一個全局的變量。當開啟緩存後,數據的查詢執行的流程就是 二級緩存 -> 一級緩存 -> 數據庫。
從上面的圖可以看出,MyBatis的二級緩存實現可以有很多種,可以是MemCache、Ehcache等。也可以是Redis等,但是需要額外的Jar包。
怎麼開啟二級緩存
二級緩存默認是不開啟的,需要手動開啟二級緩存,實現二級緩存的時候,MyBatis要求返回的POJO必須是可序列化的。開啟二級緩存的條件也是比較簡單,
step1:通過直接在 MyBatis 配置文件中通過
<settings>
<setting name = "cacheEnabled" value = "true" />
</settings>
step2: 在 Mapper 的xml 配置文件中加入
cache標籤下面有下面幾種可選項
-
eviction: 緩存回收策略,支持的策略有下面幾種
- LRU – 最近最少回收,移除最長時間不被使用的對象(默認是這個策略)
- FIFO – 先進先出,按照緩存進入的順序來移除它們
- SOFT – 軟引用,移除基於垃圾回收器狀態和軟引用規則的對象
- WEAK – 弱引用,更積極的移除基於垃圾收集器和弱引用規則的對象
-
flushinterval:緩存刷新間隔,緩存多長時間刷新一次,默認不清空,設置一個毫秒值;
-
readOnly: 是否只讀;true 只讀 ,MyBatis 認為所有從緩存中獲取數據的操作都是只讀操作,不會修改數據。MyBatis 為了加快獲取數據,直接就會將數據在緩存中的引用交給用戶。不安全,速度快。讀寫(默認):MyBatis 覺得數據可能會被修改
-
size : 緩存存放多少個元素
-
type: 指定自定義緩存的全類名(實現Cache 接口即可)
-
blocking:若緩存中找不到對應的key,是否會一直blocking,直到有對應的數據進入緩存。
cache-ref代表引用別的命名空間的Cache配置,兩個命名空間的操作使用的是同一個Cache。
哪些因素會使二級緩存失效
從上面的介紹可以知道MyBatis的二級緩存主要是為了SqlSession之間共享緩存設計的。但是我們平時開發過程中都是結合Spring來進行MyBatis的開發。在Spring環境下一般也只有一個SqlSession實例,所以二級緩存使用到的機會不多。所以下面就簡單描述下Mybatis的二級緩存。
還是以上面的列子為列
String id = "{0003CCCA-AEA9-4A1E-A3CC-06D884BA3906}";
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
//同一個sqlSession創建的Mapper
CbondissuerMapper cbondissuerMapper10 = sqlSession1.getMapper(CbondissuerMapper.class);
CbondissuerMapper cbondissuerMapper11 = sqlSession1.getMapper(CbondissuerMapper.class);
//另外一個sqlSession創建的Mapper
CbondissuerMapper cbondissuerMapper20 = sqlSession2.getMapper(CbondissuerMapper.class);
//同一個Mapper,同樣的SQL查了兩次
Cbondissuer cbondissuer10 = cbondissuerMapper10.selectByPrimaryKey(id);
Cbondissuer cbondissuer101 = cbondissuerMapper10.selectByPrimaryKey(id);
//同一個sqlSession創建的Mapper,又查詢了一次同樣的SQL
Cbondissuer cbondissuer11 = cbondissuerMapper11.selectByPrimaryKey(id);
//這邊需要提交事務才能讓二級緩存生效
sqlSession1.commit();
//不一樣的sqlSession創建的Mapper查詢了一次同樣的SQL
Cbondissuer cbondissuer20 = cbondissuerMapper20.selectByPrimaryKey(id);
System.out.println("cbondissuer10 equals cbondissuer101 :"+(cbondissuer10==cbondissuer101));
System.out.println("cbondissuer10 equals cbondissuer11 :"+(cbondissuer10==cbondissuer11));
System.out.println("cbondissuer10 equals cbondissuer21 :"+(cbondissuer10==cbondissuer20));
- 二級緩存是以namespace(Mapper)為單位的,不同namespace下的操作互不影響。
- insert,update,delete操作會清空所在namespace下的全部緩存。
- 多表操作一定不要使用二級緩存,因為多表操作進行更新操作,一定會產生臟數據。
二級緩存使用建議
個人覺得MyBatis的二級緩存實用性不是很大。一個原因就是Spring環境下,一本只有一個SqlSession,不存在sqlSession之間共享緩存;還有就是
MyBatis的緩存都不能做到分佈式,所以對於MyBatis的二級緩存以了解為主。
簡單總結
一級緩存
- 一級緩存的本質是Executor的一個類似Map的屬性;
- 一級緩存默認開啟,將flushCache設置成true或者將全局配置localCacheScope設置成Statement可以關閉一級緩存;
- 在一級緩存開啟的情況下,查詢操作會先查詢一級緩存,再查詢數據庫;
- 增刪改操作和事務提交回滾操作會導致一級緩存失效;
- 由於Spring中事務是自動提交的,因此Spring下的MyBatis一級緩存經常失效。(但是並不表示不生效,除非你手動關閉一級緩存)
- 不能實現分佈式。
二級緩存
- namesapce級別的緩存(Mapper級別或者叫做表級別的緩存),設計的主要目的是實現sqlSession之間的緩存共享;
- 開啟二級緩存後,查詢的邏輯是二級緩存->已經緩存->數據庫;
- insert,update,delete操作會清空所在namespace下的全部緩存;
- 多表查詢一定不要使用二級緩存,因為多表操作進行更新操作,可能會產生臟數據。
總體來說,MyBatis的緩存功能比較雞肋。想要使用緩存的話還是建議使用spring-cache等框架。
參考
- //blog.csdn.net/zb313982521/article/details/79689169
- //mp.weixin.qq.com/s?__biz=MzI4NDY5Mjc1Mg==&mid=2247489120&idx=2&sn=4694c4a359849d17354f85206768c25b&chksm=ebf6ce1fdc81470918515ff76c41d7aea9434226ef05e930fec59ed22dcc709030a6683c0d80&mpshare=1&scene=1&srcid=&sharer_sharetime=1566873637232&sharer_shareid=2040c1b4c62e1f430c804ebd0fe79fa3#rd