快取簡介
一般我們在系統中使用快取技術是為了提升資料查詢的效率,當我們從資料庫中查詢到一批資料后將其放入到混存中(簡單理解就是一塊記憶體區域),下次再查詢相同資料的時候就直接從快取中獲取資料就行了,這樣少了一步和資料庫的互動,可以提升查詢的效率,
但是一個硬幣都具有兩面性,快取在帶來性能提升的同時也“悄悄”引入了很多問題,比如快取同步、快取失效、快取雪崩等等,當然這些問題不是本文討論的重點,
本文主要討論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="https://www.cnblogs.com/54chensongxia/p/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 = "https://www.cnblogs.com/54chensongxia/p/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等框架,
參考
- https://blog.csdn.net/zb313982521/article/details/79689169
- https://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
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/227040.html
標籤:Java
上一篇:17張圖揭密支付寶系統架構
