前言
大家好,這一篇文章是MyBatis系列的最后一篇文章,前面兩篇文章《MyBatis源碼解析(一)—構(gòu)建篇》和《MyBatis源碼解析(二)—執(zhí)行篇》,主要說(shuō)明了MyBatis是如何將我們的xml配置文件構(gòu)建為其內(nèi)部的Configuration對(duì)象和MappedStatement對(duì)象的,然后在第二篇我們說(shuō)了構(gòu)建完成后MyBatis是如何一步一步地執(zhí)行我們的SQL語(yǔ)句并且對(duì)結(jié)果集進(jìn)行封裝的。那么這篇作為MyBatis系列的最后一篇,自然是要來(lái)聊聊MyBatis中的一個(gè)不可忽視的功能,一級(jí)緩存和二級(jí)緩存。
何謂緩存?
雖然這篇說(shuō)的是MyBatis的緩存,但是我希望正在學(xué)習(xí)計(jì)算機(jī)的小伙伴即使還沒(méi)有使用過(guò)MyBatis框架也能看明白今天這篇文章。
緩存是什么?我來(lái)說(shuō)說(shuō)個(gè)人的理解,最后再上比較官方的概念。
緩存(Cache),顧名思義,有臨時(shí)存儲(chǔ)的意思。計(jì)算機(jī)中的緩存,我們可以直接理解為,存儲(chǔ)在內(nèi)存中的數(shù)據(jù)的容器,這與物理存儲(chǔ)是有差別的,由于內(nèi)存的讀寫速度比物理存儲(chǔ)高出幾個(gè)數(shù)量級(jí),所以程序直接從內(nèi)存中取數(shù)據(jù)和從物理硬盤中取數(shù)據(jù)的效率是不同的,所以有一些經(jīng)常需要讀取的數(shù)據(jù),設(shè)計(jì)師們通常會(huì)將其放在緩存中,以便于程序?qū)ζ溥M(jìn)行讀取。但是,緩存是有代價(jià)的,剛才我們說(shuō)過(guò),緩存就是在內(nèi)存中的數(shù)據(jù)的容器,一條64G的內(nèi)存條,通??梢再I3-4塊1T-2T的機(jī)械硬盤了,所以緩存不能無(wú)節(jié)制地使用,這樣成本會(huì)劇增,所以一般緩存中的數(shù)據(jù)都是需要頻繁查詢,但是又不常修改的數(shù)據(jù)。
而在一般業(yè)務(wù)中,查詢通常會(huì)經(jīng)過(guò)如下步驟。
讀操作 --> 查詢緩存中已經(jīng)存在數(shù)據(jù) -->如果不存在則查詢數(shù)據(jù)庫(kù),如果存在則直接查詢緩存-->數(shù)據(jù)庫(kù)查詢返回?cái)?shù)據(jù)的同時(shí),寫入緩存中。
寫操作 --> 清空緩存數(shù)據(jù) -->寫入數(shù)據(jù)庫(kù)

比較官方的概念:
? 緩存就是數(shù)據(jù)交換的緩沖區(qū)(稱作:Cache),當(dāng)某一硬件要讀取數(shù)據(jù)時(shí),會(huì)首先從緩存匯總查詢數(shù)據(jù),有則直接執(zhí)行,不存在時(shí)從內(nèi)存中獲取。由于緩存的數(shù)據(jù)比內(nèi)存快的多,所以緩存的作用就是幫助硬件更快的運(yùn)行。 ? 緩存往往使用的是RAM(斷電既掉的非永久存儲(chǔ)),所以在用完后還是會(huì)把文件送到硬盤等存儲(chǔ)器中永久存儲(chǔ)。電腦中最大緩存就是內(nèi)存條,硬盤上也有16M或者32M的緩存。 ? 高速緩存是用來(lái)協(xié)調(diào)CPU與主存之間存取速度的差異而設(shè)置的。一般CPU工作速度高,但內(nèi)存的工作速度相對(duì)較低,為了解決這個(gè)問(wèn)題,通常使用高速緩存,高速緩存的存取速度介于CPU與主存之間。系統(tǒng)將一些CPU在最近幾個(gè)時(shí)間段經(jīng)常訪問(wèn)的內(nèi)容存在高速緩存,這樣就在一定程度上緩解了由于主存速度低造成的CPU“停工待料”的情況。 ? 緩存就是把一些外存上的數(shù)據(jù)保存在內(nèi)存上而已,為什么保存在內(nèi)存上,我們運(yùn)行的所有程序里面的變量都是存放在內(nèi)存中的,所以如果想將值放入內(nèi)存上,可以通過(guò)變量的方式存儲(chǔ)。在JAVA中一些緩存一般都是通過(guò)Map集合來(lái)實(shí)現(xiàn)的。
MyBatis的緩存
在說(shuō)MyBatis的緩存之前,先了解一下Java中的緩存一般都是怎么實(shí)現(xiàn)的,我們通常會(huì)使用Java中的Map,來(lái)實(shí)現(xiàn)緩存,所以在之后的緩存這個(gè)概念,就可以把它直接理解為一個(gè)Map,存的就是鍵值對(duì)。
-
一級(jí)緩存簡(jiǎn)介
MyBatis中的一級(jí)緩存,是默認(rèn)開(kāi)啟且無(wú)法關(guān)閉的,一級(jí)緩存默認(rèn)的作用域是一個(gè)SqlSession,解釋一下,就是當(dāng)SqlSession被構(gòu)建了之后,緩存就存在了,只要這個(gè)SqlSession不關(guān)閉,這個(gè)緩存就會(huì)一直存在,換言之,只要SqlSession不關(guān)閉,那么這個(gè)SqlSession處理的同一條SQL就不會(huì)被調(diào)用兩次,只有當(dāng)會(huì)話結(jié)束了之后,這個(gè)緩存才會(huì)一并被釋放。
雖說(shuō)我們不能關(guān)閉一級(jí)緩存,但是作用域是可以修改的,比如可以修改為某個(gè)Mapper。
一級(jí)緩存的生命周期:
1、如果SqlSession調(diào)用了close()方法,會(huì)釋放掉一級(jí)緩存PerpetualCache對(duì)象,一級(jí)緩存將不可用。
2、如果SqlSession調(diào)用了clearCache(),會(huì)清空PerpetualCache對(duì)象中的數(shù)據(jù),但是該對(duì)象仍可使用。
3、SqlSession中執(zhí)行了任何一個(gè)update操作(update()、delete()、insert()) ,都會(huì)清空PerpetualCache對(duì)象的數(shù)據(jù),但是該對(duì)象可以繼續(xù)使用。
節(jié)選自:https://www.cnblogs.com/happyflyingpig/p/7739749.html
MyBatis一級(jí)緩存簡(jiǎn)單示意圖 -
二級(jí)緩存簡(jiǎn)介
MyBatis的二級(jí)緩存是默認(rèn)關(guān)閉的,如果要開(kāi)啟有兩種方式:
-
在mybatis-config.xml中加入如下配置片段
<!-- 全局配置參數(shù),需要時(shí)再設(shè)置 --> <settings> <!-- 開(kāi)啟二級(jí)緩存 默認(rèn)值為true --> <setting name="cacheEnabled" value="true"/> </settings> -
在mapper.xml中開(kāi)啟
<!--開(kāi)啟本mapper的namespace下的二級(jí)緩存--> <!-- eviction:代表的是緩存回收策略,目前MyBatis提供以下策略。 (1) LRU,最近最少使用的,一處最長(zhǎng)時(shí)間不用的對(duì)象 (2) FIFO,先進(jìn)先出,按對(duì)象進(jìn)入緩存的順序來(lái)移除他們 (3) SOFT,軟引用,移除基于垃圾回收器狀態(tài)和軟引用規(guī)則的對(duì)象 (4) WEAK,弱引用,更積極的移除基于垃圾收集器狀態(tài)和弱引用規(guī)則的對(duì)象。 這里采用的是LRU, 移除最長(zhǎng)時(shí)間不用的對(duì)形象 flushInterval:刷新間隔時(shí)間,單位為毫秒,如果你不配置它,那么當(dāng) SQL被執(zhí)行的時(shí)候才會(huì)去刷新緩存。 size:引用數(shù)目,一個(gè)正整數(shù),代表緩存最多可以存儲(chǔ)多少個(gè)對(duì)象,不宜設(shè)置過(guò)大。設(shè)置過(guò)大會(huì)導(dǎo)致內(nèi)存溢出。 這里配置的是1024個(gè)對(duì)象 readOnly:只讀,意味著緩存數(shù)據(jù)只能讀取而不能修改,這樣設(shè)置的好處是我們可以快速讀取緩存,缺點(diǎn)是我們沒(méi)有 辦法修改緩存,他的默認(rèn)值是false,不允許我們修改 --> <cache eviction="回收策略" type="緩存類"/>
二級(jí)緩存的作用域與一級(jí)緩存不同,一級(jí)緩存的作用域是一個(gè)SqlSession,但是二級(jí)緩存的作用域是一個(gè)namespace,什么意思呢,你可以把它理解為一個(gè)mapper,在這個(gè)mapper中操作的所有SqlSession都可以共享這個(gè)二級(jí)緩存。但是假設(shè)有兩條相同的SQL,寫在不同的namespace下,那這個(gè)SQL就會(huì)被執(zhí)行兩次,并且產(chǎn)生兩份value相同的緩存。
-
MyBatis緩存的執(zhí)行流程
依舊是用前兩篇的測(cè)試用例,我們從源碼的角度看看緩存是如何執(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)用者角度來(lái)講 與數(shù)據(jù)庫(kù)打交道的對(duì)象 SqlSession
DemoMapper mapper = sqlSession.getMapper(DemoMapper.class);
Map<String,Object> map = new HashMap<>();
map.put("id","2121");
//執(zhí)行這個(gè)方法實(shí)際上會(huì)走到invoke
System.out.println(mapper.selectAll(map));
sqlSession.close();
sqlSession.commit();
}
這里會(huì)執(zhí)行到query()方法:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//二級(jí)緩存的Cache,通過(guò)MappedStatement獲取
Cache cache = ms.getCache();
if (cache != null) {
//是否需要刷新緩存
//在<select>標(biāo)簽中也可以配置flushCache屬性來(lái)設(shè)置是否查詢前要刷新緩存,默認(rèn)增刪改刷新緩存查詢不刷新
flushCacheIfRequired(ms);
//判斷這個(gè)mapper是否開(kāi)啟了二級(jí)緩存
if (ms.isUseCache() && resultHandler == null) {
//不管
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
//先從緩存拿
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
//如果緩存等于空,那么查詢一級(jí)緩存
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//查詢完畢后將數(shù)據(jù)放入二級(jí)緩存
tcm.putObject(cache, key, list); // issue #578 and #116
}
//返回
return list;
}
}
//如果二級(jí)緩存為null,那么直接查詢一級(jí)緩存
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
可以看到首先MyBatis在查詢數(shù)據(jù)時(shí)會(huì)先看看這個(gè)mapper是否開(kāi)啟了二級(jí)緩存,如果開(kāi)啟了,會(huì)先查詢二級(jí)緩存,如果緩存中存在我們需要的數(shù)據(jù),那么直接就從緩存返回?cái)?shù)據(jù),如果不存在,則繼續(xù)往下走查詢邏輯。
接著往下走,如果二級(jí)緩存不存在,那么就直接查詢數(shù)據(jù)了嗎?答案是否定的,二級(jí)緩存如果不存在,MyBatis會(huì)再查詢一次一級(jí)緩存,接著往下看。
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++;
//查詢一級(jí)緩存(localCache)
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
//對(duì)于存儲(chǔ)過(guò)程有輸出資源的處理
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//如果緩存為空,則從數(shù)據(jù)庫(kù)拿
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
/**這個(gè)是queryFromDatabase的邏輯
* //先往緩存中put一個(gè)占位符
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
//往一級(jí)緩存中put真實(shí)數(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;
}
一級(jí)緩存和二級(jí)緩存的查詢邏輯其實(shí)差不多,都是先查詢緩存,如果沒(méi)有則進(jìn)行下一步查詢,只不過(guò)一級(jí)緩存中如果沒(méi)有結(jié)果,那么就直接查詢數(shù)據(jù)庫(kù),然后回寫一級(jí)緩存。
講到這里其實(shí)一級(jí)緩存和二級(jí)緩存的執(zhí)行流程就說(shuō)完了,緩存的邏輯其實(shí)都差不多,MyBatis的緩存是先查詢一級(jí)緩存再查詢二級(jí)緩存。
但是文章到這里并沒(méi)有結(jié)束,還有一些緩存相關(guān)的問(wèn)題可以聊。
緩存事務(wù)問(wèn)題
不知道這個(gè)問(wèn)題大家有沒(méi)有想過(guò),假設(shè)有這么一個(gè)場(chǎng)景,這里用二級(jí)緩存舉例,因?yàn)槎?jí)緩存是跨事務(wù)的。
假設(shè)我們?cè)诓樵冎伴_(kāi)啟了事務(wù),并且進(jìn)行數(shù)據(jù)庫(kù)操作:
1.往數(shù)據(jù)庫(kù)中插入一條數(shù)據(jù)(INSERT)
2.在同一個(gè)事務(wù)內(nèi)查詢數(shù)據(jù)(SELECT)
3.提交事務(wù)(COMMIT)
4.提交事務(wù)失敗(ROLLBACK)
我們來(lái)分析一下這個(gè)場(chǎng)景,首先SqlSession先執(zhí)行了一個(gè)INSERT操作,很顯然,在我們剛才分析的邏輯基礎(chǔ)上,此時(shí)緩存一定會(huì)被清空,然后在同一個(gè)事務(wù)下查詢數(shù)據(jù),數(shù)據(jù)又從數(shù)據(jù)庫(kù)中被加載到了緩存中,此時(shí)提交事務(wù),然后事務(wù)提交失敗了??紤]一下此時(shí)會(huì)出現(xiàn)什么情況,相信已經(jīng)有人想到了,事務(wù)提交失敗之后,事務(wù)會(huì)進(jìn)行回滾,那么執(zhí)行INSERT插入的這條數(shù)據(jù)就被回滾了,但是我們?cè)诓迦胫筮M(jìn)行了一次查詢,這個(gè)數(shù)據(jù)已經(jīng)放到了緩存中,下一次查詢必然是直接查詢緩存而不會(huì)再去查詢數(shù)據(jù)庫(kù)了,可是此時(shí)緩存和數(shù)據(jù)庫(kù)之間已經(jīng)存在了數(shù)據(jù)不一致的問(wèn)題。
問(wèn)題的根本原因就在于,數(shù)據(jù)庫(kù)提交事務(wù)失敗了可以進(jìn)行回滾,但是緩存不能進(jìn)行回滾。
我們來(lái)看看MyBatis是如何解決這個(gè)問(wèn)題的。
-
TransactionalCacheManager
這個(gè)類是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中封裝了一個(gè)Map,用于將事務(wù)緩存對(duì)象緩存起來(lái),這個(gè)Map的Key是我們的二級(jí)緩存對(duì)象,而Value是一個(gè)叫做TransactionalCache,顧名思義,這個(gè)緩存就是事務(wù)緩存,我們來(lái)看看其內(nèi)部的實(shí)現(xiàn)。
public class TransactionalCache implements Cache { private static final Log log = LogFactory.getLog(TransactionalCache.class); //真實(shí)緩存對(duì)象 private final Cache delegate; //是否需要清空提交空間的標(biāo)識(shí) private boolean clearOnCommit; //所有待提交的緩存 private final Map<Object, Object> entriesToAddOnCommit; //未命中的緩存集合,防止擊穿緩存,并且如果查詢到的數(shù)據(jù)為null,說(shuō)明要通過(guò)數(shù)據(jù)庫(kù)查詢,有可能存在數(shù)據(jù)不一致,都記錄到這個(gè)地方 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ù)庫(kù)之后putObject中將本應(yīng)該放到真實(shí)緩存中的鍵值對(duì)放到待提交事務(wù)緩存 entriesMissedInCache.add(key); } //如果不為空 // issue #146 //查看緩存清空標(biāo)識(shí)是否為false,如果事務(wù)提交了就為true,事務(wù)提交了會(huì)更新緩存,所以返回null。 if (clearOnCommit) { return null; } else { //如果事務(wù)沒(méi)有提交,那么返回原先緩存中的數(shù)據(jù), return object; } } @Override public void putObject(Object key, Object object) { //如果返回的數(shù)據(jù)為null,那么有可能到數(shù)據(jù)庫(kù)查詢,查詢到的數(shù)據(jù)先放置到待提交事務(wù)的緩存中 //本來(lái)應(yīng)該put到緩存中,現(xiàn)在put到待提交事務(wù)的緩存中去。 entriesToAddOnCommit.put(key, object); } @Override public Object removeObject(Object key) { return null; } @Override public void clear() { //如果事務(wù)提交了,那么將清空緩存提交標(biāo)識(shí)設(shè)置為true clearOnCommit = true; //清空entriesToAddOnCommit entriesToAddOnCommit.clear(); } public void commit() { if (clearOnCommit) { //如果為true,那么就清空緩存。 delegate.clear(); } //把本地緩存刷新到真實(shí)緩存。 flushPendingEntries(); //然后將所有值復(fù)位。 reset(); } public void rollback() { //事務(wù)回滾 unlockMissedEntries(); reset(); } private void reset() { //復(fù)位操作。 clearOnCommit = false; entriesToAddOnCommit.clear(); entriesMissedInCache.clear(); } private void flushPendingEntries() { //遍歷事務(wù)管理器中待提交的緩存 for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) { //寫入到真實(shí)的緩存中。 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) { //清空真實(shí)緩存區(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中有一個(gè)真實(shí)緩存對(duì)象Cache,這個(gè)真實(shí)緩存對(duì)象就是我們真正的二級(jí)緩存,還有一個(gè) entriesToAddOnCommit,這個(gè)Map對(duì)象中存放的是所有待提交事務(wù)的緩存。
我們?cè)诙?jí)緩存執(zhí)行的代碼中,看到在緩存中g(shù)et或者put結(jié)果時(shí),都是叫tcm的對(duì)象調(diào)用了getObject()方法和putObject()方法,這個(gè)對(duì)象實(shí)際上就是TransactionalCacheManager的實(shí)體對(duì)象,而這個(gè)對(duì)象實(shí)際上是調(diào)用了TransactionalCache的方法,我們來(lái)看看這兩個(gè)方法是如何實(shí)現(xiàn)的。
@Override public Object getObject(Object key) { // issue #116 Object object = delegate.getObject(key); if (object == null) { //如果取出的是空,那么放到未命中緩存,并且在查詢數(shù)據(jù)庫(kù)之后putObject中將本應(yīng)該放到真實(shí)緩存中的鍵值對(duì)放到待提交事務(wù)緩存 entriesMissedInCache.add(key); } //如果不為空 // issue #146 //查看緩存清空標(biāo)識(shí)是否為false,如果事務(wù)提交了就為true,事務(wù)提交了會(huì)更新緩存,所以返回null。 if (clearOnCommit) { return null; } else { //如果事務(wù)沒(méi)有提交,那么返回原先緩存中的數(shù)據(jù), return object; } } @Override public void putObject(Object key, Object object) { //如果返回的數(shù)據(jù)為null,那么有可能到數(shù)據(jù)庫(kù)查詢,查詢到的數(shù)據(jù)先放置到待提交事務(wù)的緩存中 //本來(lái)應(yīng)該put到緩存中,現(xiàn)在put到待提交事務(wù)的緩存中去。 entriesToAddOnCommit.put(key, object); }在getObject()方法中存在兩個(gè)分支:
如果發(fā)現(xiàn)緩存中取出的數(shù)據(jù)為null,那么會(huì)把這個(gè)key放到entriesMissedInCache中,這個(gè)對(duì)象的主要作用就是將我們未命中的key全都保存下來(lái),防止緩存被擊穿,并且當(dāng)我們?cè)诰彺嬷袩o(wú)法查詢到數(shù)據(jù),那么就有可能到一級(jí)緩存和數(shù)據(jù)庫(kù)中查詢,那么查詢過(guò)后會(huì)調(diào)用putObject()方法,這個(gè)方法本應(yīng)該將我們查詢到的數(shù)據(jù)put到真是緩存中,但是現(xiàn)在由于存在事務(wù),所以暫時(shí)先放到entriesToAddOnCommit中。
如果發(fā)現(xiàn)緩存中取出的數(shù)據(jù)不為null,那么會(huì)查看事務(wù)提交標(biāo)識(shí)(clearOnCommit)是否為true,如果為true,代表事務(wù)已經(jīng)提交了,之后緩存會(huì)被清空,所以返回null,如果為false,那么由于事務(wù)還沒(méi)有被提交,所以返回當(dāng)前緩存中存的數(shù)據(jù)。
那么當(dāng)事務(wù)提交成功或提交失敗,又會(huì)是什么狀況呢?不妨看看commit和rollback方法。
public void commit() { if (clearOnCommit) { //如果為true,那么就清空緩存。 delegate.clear(); } //把本地緩存刷新到真實(shí)緩存。 flushPendingEntries(); //然后將所有值復(fù)位。 reset(); } public void rollback() { //事務(wù)回滾 unlockMissedEntries(); reset(); }先分析事務(wù)提交成功的情況,如果事務(wù)正常提交了,那么會(huì)有這么幾步操作:
- 清空真實(shí)緩存。
- 將本地緩存(未提交的事務(wù)緩存 entriesToAddOnCommit)刷新到真實(shí)緩存。
- 將所有值復(fù)位。
我們來(lái)看看代碼是如何實(shí)現(xiàn)的:
private void flushPendingEntries() { //遍歷事務(wù)管理器中待提交的緩存 for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) { //寫入到真實(shí)的緩存中。 delegate.putObject(entry.getKey(), entry.getValue()); } for (Object entry : entriesMissedInCache) { //把未命中的一起put if (!entriesToAddOnCommit.containsKey(entry)) { delegate.putObject(entry, null); } } } private void reset() { //復(fù)位操作。 clearOnCommit = false; entriesToAddOnCommit.clear(); entriesMissedInCache.clear(); } public void clear() { //如果事務(wù)提交了,那么將清空緩存提交標(biāo)識(shí)設(shè)置為true clearOnCommit = true; //清空事務(wù)提交緩存 entriesToAddOnCommit.clear(); }清空真實(shí)緩存就不說(shuō)了,就是Map調(diào)用clear方法,清空所有的鍵值對(duì)。
將未提交事務(wù)緩存刷新到真實(shí)緩存,首先會(huì)遍歷entriesToAddOnCommit,然后調(diào)用真實(shí)緩存的putObject方法,將entriesToAddOnCommit中的鍵值對(duì)put到真實(shí)緩存中,這步完成后,還會(huì)將未命中緩存中的數(shù)據(jù)一起put進(jìn)去,值設(shè)置為null。
最后進(jìn)行復(fù)位,將提交事務(wù)標(biāo)識(shí)設(shè)為false,未命中緩存、未提交事務(wù)緩存中的所有數(shù)據(jù)全都清空。
如果事務(wù)沒(méi)有正常提交,那么就會(huì)發(fā)生回滾,再來(lái)看看回滾是什么流程:
- 清空真實(shí)緩存中未命中的緩存。
- 將所有值復(fù)位
public void rollback() { //事務(wù)回滾 unlockMissedEntries(); reset(); } private void unlockMissedEntries() { for (Object entry : entriesMissedInCache) { //清空真實(shí)緩存區(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,都會(huì)被記錄到entriesMissedInCache這個(gè)緩存中,所以這個(gè)緩存中包含了所有查詢數(shù)據(jù)庫(kù)的key,所以最終只需要在真實(shí)緩存中把這部分key和對(duì)應(yīng)的value給刪除即可。
-
緩存事務(wù)總結(jié)
簡(jiǎn)而言之,緩存事務(wù)的控制主要是通過(guò)TransactionalCacheManager控制TransactionCache完成的,關(guān)鍵就在于TransactionCache中的entriesToAddCommit和entriesMissedInCache這兩個(gè)對(duì)象,entriesToAddCommit在事務(wù)開(kāi)啟到提交期間作為真實(shí)緩存的替代品,將從數(shù)據(jù)庫(kù)中查詢到的數(shù)據(jù)先放到這個(gè)Map中,待事務(wù)提交后,再將這個(gè)對(duì)象中的數(shù)據(jù)刷新到真實(shí)緩存中,如果事務(wù)提交失敗了,則清空這個(gè)緩存中的數(shù)據(jù)即可,并不會(huì)影響到真實(shí)的緩存。
entriesMissedInCache主要是用來(lái)保存在查詢過(guò)程中在緩存中沒(méi)有命中的key,由于沒(méi)有命中,說(shuō)明需要到數(shù)據(jù)庫(kù)中查詢,那么查詢過(guò)后會(huì)保存到entriesToAddCommit中,那么假設(shè)在事務(wù)提交過(guò)程中失敗了,而此時(shí)entriesToAddCommit的數(shù)據(jù)又都刷新到緩存中了,那么此時(shí)調(diào)用rollback就會(huì)通過(guò)entriesMissedInCache中保存的key,來(lái)清理真實(shí)緩存,這樣就可以保證在事務(wù)中緩存數(shù)據(jù)與數(shù)據(jù)庫(kù)的數(shù)據(jù)保持一致。
緩存事務(wù)
一些使用緩存的經(jīng)驗(yàn)
-
二級(jí)緩存不能存在一直增多的數(shù)據(jù)
由于二級(jí)緩存的影響范圍不是SqlSession而是namespace,所以二級(jí)緩存會(huì)在你的應(yīng)用啟動(dòng)時(shí)一直存在直到應(yīng)用關(guān)閉,所以二級(jí)緩存中不能存在隨著時(shí)間數(shù)據(jù)量越來(lái)越大的數(shù)據(jù),這樣有可能會(huì)造成內(nèi)存空間被占滿。
-
二級(jí)緩存有可能存在臟讀的問(wèn)題(可避免)
由于二級(jí)緩存的作用域?yàn)閚amespace,那么就可以假設(shè)這么一個(gè)場(chǎng)景,有兩個(gè)namespace操作一張表,第一個(gè)namespace查詢?cè)摫聿⒒貙懙絻?nèi)存中,第二個(gè)namespace往表中插一條數(shù)據(jù),那么第一個(gè)namespace的二級(jí)緩存是不會(huì)清空這個(gè)緩存的內(nèi)容的,在下一次查詢中,還會(huì)通過(guò)緩存去查詢,這樣會(huì)造成數(shù)據(jù)的不一致。
所以當(dāng)項(xiàng)目里有多個(gè)命名空間操作同一張表的時(shí)候,最好不要用二級(jí)緩存,或者使用二級(jí)緩存時(shí)避免用兩個(gè)namespace操作一張表。
-
Spring整合MyBatis緩存失效問(wèn)題
一級(jí)緩存的作用域是SqlSession,而使用者可以自定義SqlSession什么時(shí)候出現(xiàn)什么時(shí)候銷毀,在這段期間一級(jí)緩存都是存在的。
當(dāng)使用者調(diào)用close()方法之后,就會(huì)銷毀一級(jí)緩存。但是,我們?cè)诤蚐pring整合之后,Spring幫我們跳過(guò)了SqlSessionFactory這一步,我們可以直接調(diào)用Mapper,導(dǎo)致在操作完數(shù)據(jù)庫(kù)之后,Spring就將SqlSession就銷毀了,一級(jí)緩存就隨之銷毀了,所以一級(jí)緩存就失效了。
那么怎么能讓緩存生效呢?
1.開(kāi)啟事務(wù),因?yàn)橐坏╅_(kāi)啟事務(wù),Spring就不會(huì)在執(zhí)行完SQL之后就銷毀SqlSession,因?yàn)镾qlSession一旦關(guān)閉,事務(wù)就沒(méi)了,一旦我們開(kāi)啟事務(wù),在事務(wù)期間內(nèi),緩存會(huì)一直存在。
2.使用二級(jí)緩存。
結(jié)語(yǔ)
Hello world.
歡迎大家訪問(wèn)我的個(gè)人博客:Object's Blog

