前言
大家好,這一篇文章是MyBatis系列的最后一篇文章戳气,前面兩篇文章《MyBatis源碼解析(一)—構(gòu)建篇》和《MyBatis源碼解析(二)—執(zhí)行篇》,主要說明了MyBatis是如何將我們的xml配置文件構(gòu)建為其內(nèi)部的Configuration對象和MappedStatement對象的巧鸭,然后在第二篇我們說了構(gòu)建完成后MyBatis是如何一步一步地執(zhí)行我們的SQL語句并且對結(jié)果集進行封裝的瓶您。那么這篇作為MyBatis系列的最后一篇,自然是要來聊聊MyBatis中的一個不可忽視的功能纲仍,一級緩存和二級緩存览闰。
何謂緩存?
雖然這篇說的是MyBatis的緩存巷折,但是我希望正在學習計算機的小伙伴即使還沒有使用過MyBatis框架也能看明白今天這篇文章压鉴。
緩存是什么?我來說說個人的理解锻拘,最后再上比較官方的概念油吭。
緩存(Cache),顧名思義署拟,有臨時存儲的意思婉宰。計算機中的緩存,我們可以直接理解為推穷,存儲在內(nèi)存中的數(shù)據(jù)的容器心包,這與物理存儲是有差別的,由于內(nèi)存的讀寫速度比物理存儲高出幾個數(shù)量級馒铃,所以程序直接從內(nèi)存中取數(shù)據(jù)和從物理硬盤中取數(shù)據(jù)的效率是不同的蟹腾,所以有一些經(jīng)常需要讀取的數(shù)據(jù),設(shè)計師們通常會將其放在緩存中区宇,以便于程序?qū)ζ溥M行讀取娃殖。但是,緩存是有代價的议谷,剛才我們說過炉爆,緩存就是在內(nèi)存中的數(shù)據(jù)的容器,一條64G的內(nèi)存條卧晓,通撤沂祝可以買3-4塊1T-2T的機械硬盤了,所以緩存不能無節(jié)制地使用逼裆,這樣成本會劇增郁稍,所以一般緩存中的數(shù)據(jù)都是需要頻繁查詢,但是又不常修改的數(shù)據(jù)波附。
而在一般業(yè)務(wù)中艺晴,查詢通常會經(jīng)過如下步驟昼钻。
讀操作 --> 查詢緩存中已經(jīng)存在數(shù)據(jù) -->如果不存在則查詢數(shù)據(jù)庫掸屡,如果存在則直接查詢緩存-->數(shù)據(jù)庫查詢返回數(shù)據(jù)的同時封寞,寫入緩存中。
寫操作 --> 清空緩存數(shù)據(jù) -->寫入數(shù)據(jù)庫
比較官方的概念:
? 緩存就是數(shù)據(jù)交換的緩沖區(qū)(稱作:Cache)仅财,當某一硬件要讀取數(shù)據(jù)時狈究,會首先從緩存匯總查詢數(shù)據(jù),有則直接執(zhí)行盏求,不存在時從內(nèi)存中獲取抖锥。由于緩存的數(shù)據(jù)比內(nèi)存快的多,所以緩存的作用就是幫助硬件更快的運行碎罚。 ? 緩存往往使用的是RAM(斷電既掉的非永久存儲)磅废,所以在用完后還是會把文件送到硬盤等存儲器中永久存儲。電腦中最大緩存就是內(nèi)存條荆烈,硬盤上也有16M或者32M的緩存拯勉。 ? 高速緩存是用來協(xié)調(diào)CPU與主存之間存取速度的差異而設(shè)置的。一般CPU工作速度高憔购,但內(nèi)存的工作速度相對較低宫峦,為了解決這個問題,通常使用高速緩存玫鸟,高速緩存的存取速度介于CPU與主存之間导绷。系統(tǒng)將一些CPU在最近幾個時間段經(jīng)常訪問的內(nèi)容存在高速緩存,這樣就在一定程度上緩解了由于主存速度低造成的CPU“停工待料”的情況屎飘。 ? 緩存就是把一些外存上的數(shù)據(jù)保存在內(nèi)存上而已妥曲,為什么保存在內(nèi)存上,我們運行的所有程序里面的變量都是存放在內(nèi)存中的钦购,所以如果想將值放入內(nèi)存上逾一,可以通過變量的方式存儲。在JAVA中一些緩存一般都是通過Map集合來實現(xiàn)的肮雨。
MyBatis的緩存
在說MyBatis的緩存之前遵堵,先了解一下Java中的緩存一般都是怎么實現(xiàn)的,我們通常會使用Java中的Map怨规,來實現(xiàn)緩存陌宿,所以在之后的緩存這個概念,就可以把它直接理解為一個Map波丰,存的就是鍵值對壳坪。
-
一級緩存簡介
MyBatis中的一級緩存,是默認開啟且無法關(guān)閉的掰烟,一級緩存默認的作用域是一個SqlSession爽蝴,解釋一下沐批,就是當SqlSession被構(gòu)建了之后,緩存就存在了蝎亚,只要這個SqlSession不關(guān)閉九孩,這個緩存就會一直存在,換言之发框,只要SqlSession不關(guān)閉躺彬,那么這個SqlSession處理的同一條SQL就不會被調(diào)用兩次,只有當會話結(jié)束了之后梅惯,這個緩存才會一并被釋放宪拥。
雖說我們不能關(guān)閉一級緩存,但是作用域是可以修改的铣减,比如可以修改為某個Mapper她君。
一級緩存的生命周期:
1、如果SqlSession調(diào)用了close()方法葫哗,會釋放掉一級緩存PerpetualCache對象缔刹,一級緩存將不可用。
2魄梯、如果SqlSession調(diào)用了clearCache()桨螺,會清空PerpetualCache對象中的數(shù)據(jù),但是該對象仍可使用酿秸。
3灭翔、SqlSession中執(zhí)行了任何一個update操作(update()、delete()辣苏、insert()) 肝箱,都會清空PerpetualCache對象的數(shù)據(jù),但是該對象可以繼續(xù)使用稀蟋。
節(jié)選自:https://www.cnblogs.com/happyflyingpig/p/7739749.html
MyBatis一級緩存簡單示意圖 -
二級緩存簡介
MyBatis的二級緩存是默認關(guān)閉的煌张,如果要開啟有兩種方式:
-
在mybatis-config.xml中加入如下配置片段
<!-- 全局配置參數(shù),需要時再設(shè)置 --> <settings> <!-- 開啟二級緩存 默認值為true --> <setting name="cacheEnabled" value="true"/> </settings>
-
在mapper.xml中開啟
<!--開啟本mapper的namespace下的二級緩存--> <!-- eviction:代表的是緩存回收策略退客,目前MyBatis提供以下策略骏融。 (1) LRU,最近最少使用的,一處最長時間不用的對象 (2) FIFO,先進先出萌狂,按對象進入緩存的順序來移除他們 (3) SOFT,軟引用档玻,移除基于垃圾回收器狀態(tài)和軟引用規(guī)則的對象 (4) WEAK,弱引用,更積極的移除基于垃圾收集器狀態(tài)和弱引用規(guī)則的對象茫藏。 這里采用的是LRU误趴, 移除最長時間不用的對形象 flushInterval:刷新間隔時間,單位為毫秒务傲,如果你不配置它凉当,那么當 SQL被執(zhí)行的時候才會去刷新緩存枣申。 size:引用數(shù)目,一個正整數(shù)看杭,代表緩存最多可以存儲多少個對象忠藤,不宜設(shè)置過大。設(shè)置過大會導致內(nèi)存溢出泊窘。 這里配置的是1024個對象 readOnly:只讀熄驼,意味著緩存數(shù)據(jù)只能讀取而不能修改像寒,這樣設(shè)置的好處是我們可以快速讀取緩存烘豹,缺點是我們沒有 辦法修改緩存,他的默認值是false诺祸,不允許我們修改 --> <cache eviction="回收策略" type="緩存類"/>
二級緩存的作用域與一級緩存不同携悯,一級緩存的作用域是一個SqlSession,但是二級緩存的作用域是一個namespace筷笨,什么意思呢憔鬼,你可以把它理解為一個mapper,在這個mapper中操作的所有SqlSession都可以共享這個二級緩存胃夏。但是假設(shè)有兩條相同的SQL轴或,寫在不同的namespace下,那這個SQL就會被執(zhí)行兩次仰禀,并且產(chǎn)生兩份value相同的緩存照雁。
-
MyBatis緩存的執(zhí)行流程
依舊是用前兩篇的測試用例,我們從源碼的角度看看緩存是如何執(zhí)行的答恶。
public static void main(String[] args) throws Exception {
String resource = "mybatis.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
//從調(diào)用者角度來講 與數(shù)據(jù)庫打交道的對象 SqlSession
DemoMapper mapper = sqlSession.getMapper(DemoMapper.class);
Map<String,Object> map = new HashMap<>();
map.put("id","2121");
//執(zhí)行這個方法實際上會走到invoke
System.out.println(mapper.selectAll(map));
sqlSession.close();
sqlSession.commit();
}
這里會執(zhí)行到query()方法:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//二級緩存的Cache,通過MappedStatement獲取
Cache cache = ms.getCache();
if (cache != null) {
//是否需要刷新緩存
//在<select>標簽中也可以配置flushCache屬性來設(shè)置是否查詢前要刷新緩存饺蚊,默認增刪改刷新緩存查詢不刷新
flushCacheIfRequired(ms);
//判斷這個mapper是否開啟了二級緩存
if (ms.isUseCache() && resultHandler == null) {
//不管
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
//先從緩存拿
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
//如果緩存等于空,那么查詢一級緩存
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//查詢完畢后將數(shù)據(jù)放入二級緩存
tcm.putObject(cache, key, list); // issue #578 and #116
}
//返回
return list;
}
}
//如果二級緩存為null悬嗓,那么直接查詢一級緩存
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
可以看到首先MyBatis在查詢數(shù)據(jù)時會先看看這個mapper是否開啟了二級緩存污呼,如果開啟了,會先查詢二級緩存包竹,如果緩存中存在我們需要的數(shù)據(jù)燕酷,那么直接就從緩存返回數(shù)據(jù),如果不存在周瞎,則繼續(xù)往下走查詢邏輯苗缩。
接著往下走,如果二級緩存不存在堰氓,那么就直接查詢數(shù)據(jù)了嗎挤渐?答案是否定的,二級緩存如果不存在双絮,MyBatis會再查詢一次一級緩存浴麻,接著往下看得问。
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++;
//查詢一級緩存(localCache)
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
//對于存儲過程有輸出資源的處理
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//如果緩存為空,則從數(shù)據(jù)庫拿
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
/**這個是queryFromDatabase的邏輯
* //先往緩存中put一個占位符
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
//往一級緩存中put真實數(shù)據(jù)
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
*/
}
} 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;
}
一級緩存和二級緩存的查詢邏輯其實差不多软免,都是先查詢緩存宫纬,如果沒有則進行下一步查詢,只不過一級緩存中如果沒有結(jié)果膏萧,那么就直接查詢數(shù)據(jù)庫漓骚,然后回寫一級緩存。
講到這里其實一級緩存和二級緩存的執(zhí)行流程就說完了榛泛,緩存的邏輯其實都差不多蝌蹂,MyBatis的緩存是先查詢一級緩存再查詢二級緩存。
但是文章到這里并沒有結(jié)束曹锨,還有一些緩存相關(guān)的問題可以聊孤个。
緩存事務(wù)問題
不知道這個問題大家有沒有想過,假設(shè)有這么一個場景沛简,這里用二級緩存舉例齐鲤,因為二級緩存是跨事務(wù)的。
假設(shè)我們在查詢之前開啟了事務(wù)椒楣,并且進行數(shù)據(jù)庫操作:
1.往數(shù)據(jù)庫中插入一條數(shù)據(jù)(INSERT)
2.在同一個事務(wù)內(nèi)查詢數(shù)據(jù)(SELECT)
3.提交事務(wù)(COMMIT)
4.提交事務(wù)失敗(ROLLBACK)
我們來分析一下這個場景给郊,首先SqlSession先執(zhí)行了一個INSERT操作,很顯然捧灰,在我們剛才分析的邏輯基礎(chǔ)上淆九,此時緩存一定會被清空,然后在同一個事務(wù)下查詢數(shù)據(jù)凤壁,數(shù)據(jù)又從數(shù)據(jù)庫中被加載到了緩存中吩屹,此時提交事務(wù),然后事務(wù)提交失敗了拧抖∶核眩考慮一下此時會出現(xiàn)什么情況,相信已經(jīng)有人想到了唧席,事務(wù)提交失敗之后擦盾,事務(wù)會進行回滾,那么執(zhí)行INSERT插入的這條數(shù)據(jù)就被回滾了淌哟,但是我們在插入之后進行了一次查詢迹卢,這個數(shù)據(jù)已經(jīng)放到了緩存中,下一次查詢必然是直接查詢緩存而不會再去查詢數(shù)據(jù)庫了徒仓,可是此時緩存和數(shù)據(jù)庫之間已經(jīng)存在了數(shù)據(jù)不一致的問題腐碱。
問題的根本原因就在于,數(shù)據(jù)庫提交事務(wù)失敗了可以進行回滾,但是緩存不能進行回滾症见。
我們來看看MyBatis是如何解決這個問題的喂走。
-
TransactionalCacheManager
這個類是MyBatis用于緩存事務(wù)管理的類,我們可以看看其數(shù)據(jù)結(jié)構(gòu)谋作。
public class TransactionalCacheManager { //事務(wù)緩存 private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>(); public void clear(Cache cache) { getTransactionalCache(cache).clear(); } public Object getObject(Cache cache, CacheKey key) { return getTransactionalCache(cache).getObject(key); } public void putObject(Cache cache, CacheKey key, Object value) { getTransactionalCache(cache).putObject(key, value); } public void commit() { for (TransactionalCache txCache : transactionalCaches.values()) { txCache.commit(); } } public void rollback() { for (TransactionalCache txCache : transactionalCaches.values()) { txCache.rollback(); } } private TransactionalCache getTransactionalCache(Cache cache) { return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new); } }
TransactionalCacheManager中封裝了一個Map芋肠,用于將事務(wù)緩存對象緩存起來,這個Map的Key是我們的二級緩存對象遵蚜,而Value是一個叫做TransactionalCache帖池,顧名思義,這個緩存就是事務(wù)緩存吭净,我們來看看其內(nèi)部的實現(xiàn)睡汹。
public class TransactionalCache implements Cache { private static final Log log = LogFactory.getLog(TransactionalCache.class); //真實緩存對象 private final Cache delegate; //是否需要清空提交空間的標識 private boolean clearOnCommit; //所有待提交的緩存 private final Map<Object, Object> entriesToAddOnCommit; //未命中的緩存集合,防止擊穿緩存攒钳,并且如果查詢到的數(shù)據(jù)為null帮孔,說明要通過數(shù)據(jù)庫查詢雷滋,有可能存在數(shù)據(jù)不一致不撑,都記錄到這個地方 private final Set<Object> entriesMissedInCache; public TransactionalCache(Cache delegate) { this.delegate = delegate; this.clearOnCommit = false; this.entriesToAddOnCommit = new HashMap<>(); this.entriesMissedInCache = new HashSet<>(); } @Override public String getId() { return delegate.getId(); } @Override public int getSize() { return delegate.getSize(); } @Override public Object getObject(Object key) { // issue #116 Object object = delegate.getObject(key); if (object == null) { //如果取出的是空,那么放到未命中緩存晤斩,并且在查詢數(shù)據(jù)庫之后putObject中將本應該放到真實緩存中的鍵值對放到待提交事務(wù)緩存 entriesMissedInCache.add(key); } //如果不為空 // issue #146 //查看緩存清空標識是否為false焕檬,如果事務(wù)提交了就為true,事務(wù)提交了會更新緩存澳泵,所以返回null实愚。 if (clearOnCommit) { return null; } else { //如果事務(wù)沒有提交,那么返回原先緩存中的數(shù)據(jù)兔辅, return object; } } @Override public void putObject(Object key, Object object) { //如果返回的數(shù)據(jù)為null腊敲,那么有可能到數(shù)據(jù)庫查詢,查詢到的數(shù)據(jù)先放置到待提交事務(wù)的緩存中 //本來應該put到緩存中维苔,現(xiàn)在put到待提交事務(wù)的緩存中去碰辅。 entriesToAddOnCommit.put(key, object); } @Override public Object removeObject(Object key) { return null; } @Override public void clear() { //如果事務(wù)提交了,那么將清空緩存提交標識設(shè)置為true clearOnCommit = true; //清空entriesToAddOnCommit entriesToAddOnCommit.clear(); } public void commit() { if (clearOnCommit) { //如果為true介时,那么就清空緩存没宾。 delegate.clear(); } //把本地緩存刷新到真實緩存。 flushPendingEntries(); //然后將所有值復位沸柔。 reset(); } public void rollback() { //事務(wù)回滾 unlockMissedEntries(); reset(); } private void reset() { //復位操作循衰。 clearOnCommit = false; entriesToAddOnCommit.clear(); entriesMissedInCache.clear(); } private void flushPendingEntries() { //遍歷事務(wù)管理器中待提交的緩存 for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) { //寫入到真實的緩存中。 delegate.putObject(entry.getKey(), entry.getValue()); } for (Object entry : entriesMissedInCache) { //把未命中的一起put if (!entriesToAddOnCommit.containsKey(entry)) { delegate.putObject(entry, null); } } } private void unlockMissedEntries() { for (Object entry : entriesMissedInCache) { //清空真實緩存區(qū)中未命中的緩存褐澎。 try { delegate.removeObject(entry); } catch (Exception e) { log.warn("Unexpected exception while notifiying a rollback to the cache adapter." + "Consider upgrading your cache adapter to the latest version. Cause: " + e); } } } }
在TransactionalCache中有一個真實緩存對象Cache会钝,這個真實緩存對象就是我們真正的二級緩存,還有一個 entriesToAddOnCommit工三,這個Map對象中存放的是所有待提交事務(wù)的緩存迁酸。
我們在二級緩存執(zhí)行的代碼中咽弦,看到在緩存中g(shù)et或者put結(jié)果時,都是叫tcm的對象調(diào)用了getObject()方法和putObject()方法胁出,這個對象實際上就是TransactionalCacheManager的實體對象型型,而這個對象實際上是調(diào)用了TransactionalCache的方法,我們來看看這兩個方法是如何實現(xiàn)的全蝶。
@Override public Object getObject(Object key) { // issue #116 Object object = delegate.getObject(key); if (object == null) { //如果取出的是空闹蒜,那么放到未命中緩存,并且在查詢數(shù)據(jù)庫之后putObject中將本應該放到真實緩存中的鍵值對放到待提交事務(wù)緩存 entriesMissedInCache.add(key); } //如果不為空 // issue #146 //查看緩存清空標識是否為false抑淫,如果事務(wù)提交了就為true绷落,事務(wù)提交了會更新緩存,所以返回null始苇。 if (clearOnCommit) { return null; } else { //如果事務(wù)沒有提交砌烁,那么返回原先緩存中的數(shù)據(jù), return object; } } @Override public void putObject(Object key, Object object) { //如果返回的數(shù)據(jù)為null催式,那么有可能到數(shù)據(jù)庫查詢函喉,查詢到的數(shù)據(jù)先放置到待提交事務(wù)的緩存中 //本來應該put到緩存中,現(xiàn)在put到待提交事務(wù)的緩存中去荣月。 entriesToAddOnCommit.put(key, object); }
在getObject()方法中存在兩個分支:
如果發(fā)現(xiàn)緩存中取出的數(shù)據(jù)為null管呵,那么會把這個key放到entriesMissedInCache中,這個對象的主要作用就是將我們未命中的key全都保存下來哺窄,防止緩存被擊穿捐下,并且當我們在緩存中無法查詢到數(shù)據(jù),那么就有可能到一級緩存和數(shù)據(jù)庫中查詢萌业,那么查詢過后會調(diào)用putObject()方法坷襟,這個方法本應該將我們查詢到的數(shù)據(jù)put到真是緩存中,但是現(xiàn)在由于存在事務(wù)生年,所以暫時先放到entriesToAddOnCommit中婴程。
如果發(fā)現(xiàn)緩存中取出的數(shù)據(jù)不為null,那么會查看事務(wù)提交標識(clearOnCommit)是否為true晶框,如果為true排抬,代表事務(wù)已經(jīng)提交了,之后緩存會被清空授段,所以返回null蹲蒲,如果為false,那么由于事務(wù)還沒有被提交侵贵,所以返回當前緩存中存的數(shù)據(jù)届搁。
那么當事務(wù)提交成功或提交失敗,又會是什么狀況呢?不妨看看commit和rollback方法卡睦。
public void commit() { if (clearOnCommit) { //如果為true宴胧,那么就清空緩存。 delegate.clear(); } //把本地緩存刷新到真實緩存表锻。 flushPendingEntries(); //然后將所有值復位恕齐。 reset(); } public void rollback() { //事務(wù)回滾 unlockMissedEntries(); reset(); }
先分析事務(wù)提交成功的情況,如果事務(wù)正常提交了瞬逊,那么會有這么幾步操作:
- 清空真實緩存显歧。
- 將本地緩存(未提交的事務(wù)緩存 entriesToAddOnCommit)刷新到真實緩存。
- 將所有值復位确镊。
我們來看看代碼是如何實現(xiàn)的:
private void flushPendingEntries() { //遍歷事務(wù)管理器中待提交的緩存 for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) { //寫入到真實的緩存中士骤。 delegate.putObject(entry.getKey(), entry.getValue()); } for (Object entry : entriesMissedInCache) { //把未命中的一起put if (!entriesToAddOnCommit.containsKey(entry)) { delegate.putObject(entry, null); } } } private void reset() { //復位操作。 clearOnCommit = false; entriesToAddOnCommit.clear(); entriesMissedInCache.clear(); } public void clear() { //如果事務(wù)提交了蕾域,那么將清空緩存提交標識設(shè)置為true clearOnCommit = true; //清空事務(wù)提交緩存 entriesToAddOnCommit.clear(); }
清空真實緩存就不說了拷肌,就是Map調(diào)用clear方法,清空所有的鍵值對旨巷。
將未提交事務(wù)緩存刷新到真實緩存巨缘,首先會遍歷entriesToAddOnCommit,然后調(diào)用真實緩存的putObject方法契沫,將entriesToAddOnCommit中的鍵值對put到真實緩存中带猴,這步完成后,還會將未命中緩存中的數(shù)據(jù)一起put進去懈万,值設(shè)置為null。
最后進行復位靶病,將提交事務(wù)標識設(shè)為false会通,未命中緩存、未提交事務(wù)緩存中的所有數(shù)據(jù)全都清空娄周。
如果事務(wù)沒有正常提交涕侈,那么就會發(fā)生回滾,再來看看回滾是什么流程:
- 清空真實緩存中未命中的緩存煤辨。
- 將所有值復位
public void rollback() { //事務(wù)回滾 unlockMissedEntries(); reset(); } private void unlockMissedEntries() { for (Object entry : entriesMissedInCache) { //清空真實緩存區(qū)中未命中的緩存裳涛。 try { delegate.removeObject(entry); } catch (Exception e) { log.warn("Unexpected exception while notifiying a rollback to the cache adapter." + "Consider upgrading your cache adapter to the latest version. Cause: " + e); } } }
由于凡是在緩存中未命中的key,都會被記錄到entriesMissedInCache這個緩存中众辨,所以這個緩存中包含了所有查詢數(shù)據(jù)庫的key端三,所以最終只需要在真實緩存中把這部分key和對應的value給刪除即可。
-
緩存事務(wù)總結(jié)
簡而言之鹃彻,緩存事務(wù)的控制主要是通過TransactionalCacheManager控制TransactionCache完成的郊闯,關(guān)鍵就在于TransactionCache中的entriesToAddCommit和entriesMissedInCache這兩個對象,entriesToAddCommit在事務(wù)開啟到提交期間作為真實緩存的替代品,將從數(shù)據(jù)庫中查詢到的數(shù)據(jù)先放到這個Map中团赁,待事務(wù)提交后育拨,再將這個對象中的數(shù)據(jù)刷新到真實緩存中,如果事務(wù)提交失敗了欢摄,則清空這個緩存中的數(shù)據(jù)即可熬丧,并不會影響到真實的緩存。
entriesMissedInCache主要是用來保存在查詢過程中在緩存中沒有命中的key怀挠,由于沒有命中锹引,說明需要到數(shù)據(jù)庫中查詢,那么查詢過后會保存到entriesToAddCommit中唆香,那么假設(shè)在事務(wù)提交過程中失敗了嫌变,而此時entriesToAddCommit的數(shù)據(jù)又都刷新到緩存中了,那么此時調(diào)用rollback就會通過entriesMissedInCache中保存的key躬它,來清理真實緩存腾啥,這樣就可以保證在事務(wù)中緩存數(shù)據(jù)與數(shù)據(jù)庫的數(shù)據(jù)保持一致。
緩存事務(wù)
一些使用緩存的經(jīng)驗
-
二級緩存不能存在一直增多的數(shù)據(jù)
由于二級緩存的影響范圍不是SqlSession而是namespace冯吓,所以二級緩存會在你的應用啟動時一直存在直到應用關(guān)閉倘待,所以二級緩存中不能存在隨著時間數(shù)據(jù)量越來越大的數(shù)據(jù),這樣有可能會造成內(nèi)存空間被占滿组贺。
-
二級緩存有可能存在臟讀的問題(可避免)
由于二級緩存的作用域為namespace凸舵,那么就可以假設(shè)這么一個場景,有兩個namespace操作一張表失尖,第一個namespace查詢該表并回寫到內(nèi)存中啊奄,第二個namespace往表中插一條數(shù)據(jù),那么第一個namespace的二級緩存是不會清空這個緩存的內(nèi)容的掀潮,在下一次查詢中菇夸,還會通過緩存去查詢,這樣會造成數(shù)據(jù)的不一致仪吧。
所以當項目里有多個命名空間操作同一張表的時候庄新,最好不要用二級緩存,或者使用二級緩存時避免用兩個namespace操作一張表薯鼠。
-
Spring整合MyBatis緩存失效問題
一級緩存的作用域是SqlSession择诈,而使用者可以自定義SqlSession什么時候出現(xiàn)什么時候銷毀,在這段期間一級緩存都是存在的出皇。
當使用者調(diào)用close()方法之后羞芍,就會銷毀一級緩存。但是恶迈,我們在和Spring整合之后涩金,Spring幫我們跳過了SqlSessionFactory這一步谱醇,我們可以直接調(diào)用Mapper,導致在操作完數(shù)據(jù)庫之后步做,Spring就將SqlSession就銷毀了副渴,一級緩存就隨之銷毀了,所以一級緩存就失效了全度。
那么怎么能讓緩存生效呢煮剧?
1.開啟事務(wù),因為一旦開啟事務(wù)将鸵,Spring就不會在執(zhí)行完SQL之后就銷毀SqlSession勉盅,因為SqlSession一旦關(guān)閉,事務(wù)就沒了顶掉,一旦我們開啟事務(wù)草娜,在事務(wù)期間內(nèi),緩存會一直存在痒筒。
2.使用二級緩存宰闰。
結(jié)語
Hello world.
歡迎大家訪問我的個人博客:Object's Blog