上一篇我們介紹了mybatis的二級緩存作用范圍, 二級緩存與一級緩存的結(jié)構(gòu)關(guān)系, 今天就來介紹二級緩存本身是如何實(shí)現(xiàn)的~ 友情提示: 搭配 [源碼]mybatis二級緩存源碼分析(一)----一級緩存與二級緩存的結(jié)構(gòu)關(guān)系 食用更香嚎卫。
NO.1 |如何開啟二級緩存
開啟二級緩存的方式也比較簡單, 如下:
第一步: MyBatis 配置文件中配置<settings> <setting name = "cacheEnabled" value = "true" /></settings>第二步: 在Mapper.xml文件中配置<cache/>標(biāo)簽, 一個Mapper.xml文件擁有唯一的namespace(命名空間)<cache type="org.apache.ibatis.cache.impl.PerpetualCache" size="1024" eviction="LRU" flushInterval="120000" readOnly="false" />也可以配置<cache-ref/>,<cache-ref/>標(biāo)簽是為了引用其他的命名空間菱农,那么當(dāng)前命名空間將與引用的命名空間使用同一個緩存(對于同一命名空間下的多表查詢可借助該標(biāo)簽避免臟讀問題)
1.標(biāo)簽屬性含義
在開啟二級緩存的第二步中, 要在Mapper.xml文件中配置標(biāo)簽, 同時也可以為標(biāo)簽擁有的屬性賦值, 那標(biāo)簽的屬性們的含義都是什么?
type -代表著緩存的默認(rèn)實(shí)現(xiàn);size -代表緩存容量;eviction-代表溢出淘汰策略;flushInterval-代表緩存有效期;readOnly- 是否只讀谨敛,若配置可讀寫尊蚁,則需要對應(yīng)的實(shí)體類能夠序列化;blocking- 若緩存中找不到對應(yīng)的key, 是否一直阻塞, 直到有對應(yīng)的數(shù)據(jù)放入緩存;
2.產(chǎn)生的效果
做了如上配置后產(chǎn)生的效果如下
a.映射語句文件中的所有 select 操作的結(jié)果將會被緩存。b.映射語句文件中的所有 update操作( insert 吃嘿、update 和 delete )會刷新緩存祠乃。c.緩存會使用最近最少使用(LRU, Least Recently Used)算法來淘汰不需要的緩存。d.緩存會間隔120000ms后清空一次緩存兑燥。e.緩存會保存列表或?qū)ο螅o論查詢方法返回哪種)的 1024 個引用亮瓷。f.緩存會被視為讀寫緩存, 需要查詢出來要被緩存的實(shí)體類實(shí)現(xiàn)Serializable接口。 這意味著獲取到的對象并不是共享的贪嫂,可以安全地被調(diào)用者修改, 而不干擾其他調(diào)用者或線程 寺庄。
關(guān)于readOnly="false"為何需要查詢出來的緩存實(shí)體類實(shí)現(xiàn)序列化接口:
這是因?yàn)槎壘彺鏋榱吮WC讀寫安全, 開啟了序列化功能, 緩存中保存的不再是查詢出的對象本身, 而是查詢出的對象進(jìn)行序列化后的字節(jié)序列, 在獲取數(shù)據(jù)的時候, 又會把存好的字節(jié)序列進(jìn)行反序列化, 克隆出新對象, 進(jìn)行返回艾蓝。
所以對從二級緩存中得到數(shù)據(jù)做任何寫操作, 都不會影響到緩存中原有的對象, 也就不會影響到其他來獲取數(shù)據(jù)的調(diào)用者或線程力崇。
tips: Java序列化就是指把Java對象轉(zhuǎn)換為字節(jié)序列的過程斗塘。Java反序列化就是指把字節(jié)序列恢復(fù)為Java對象的過程。而在反序列化的時候會根據(jù)字節(jié)序列中保存的對象狀態(tài)及描述信息, 重建對象亮靴。
NO.2 |二級緩存組件結(jié)構(gòu)
從以上的描述中我們看出, Mybatis的二級緩存要實(shí)現(xiàn)的功能更加復(fù)雜, 比如: 線程安全, 過期清理, 命中率統(tǒng)計, 序列化….
Mybatis為了盡可能的職責(zé)分明的實(shí)現(xiàn)這些復(fù)雜邏輯, 在這里使用了一種設(shè)計模式: 裝飾者+ 責(zé)任鏈(變種), 對二級緩存的功能組件進(jìn)行設(shè)計馍盟。至于為什么說是一個責(zé)任鏈變種, 我們需要先了解一下經(jīng)典責(zé)任鏈的定義。
tips: 責(zé)任鏈: (經(jīng)典定義) 是一個請求有多個對象來處理茧吊,這些對象是一條鏈贞岭,但具體由哪個對象來處理,根據(jù)條件判斷來確定搓侄,如果不能處理會傳遞給該鏈中的下一個對象瞄桨,直到有對象處理它為止。
而責(zé)任鏈中的鏈, 是如何形成的呢? 舉一個栗子, 比如我們的鏈?zhǔn)浇Y(jié)構(gòu)是a對象->b對象->c對象, 那我們就讓a對象持有b對象, b對象持有c對象讶踪。從a對象開始, a對象的方法中可以調(diào)用b對象的方法, 而b對象的方法中也可以調(diào)用c對象的方法, 通過這樣的方式, 便形成了一條責(zé)任鏈芯侥。
經(jīng)典責(zé)任鏈的方式, 要根據(jù)條件判斷, 雖然也許會經(jīng)過鏈條上的很多對象, 但最終只有一個對象真正對請求進(jìn)行了處理, 其他對象僅僅完成了向下傳遞。Mybatis的二級緩存使用的責(zé)任鏈模式則不同, 每一個鏈條上的對象不僅要調(diào)用自身持有的對象的方法, 完成了責(zé)任鏈的向下傳遞, 也要完成自己的功能實(shí)現(xiàn)乳讥。所以說Mybatis使用的是責(zé)任鏈的變種形式柱查。
二級緩存的組件結(jié)構(gòu)如下圖所示:
二級緩存組件的頂級接口是Cache, 定義了二級緩存的api, 比如設(shè)置緩存, 取出緩存。Cache下方有很多實(shí)現(xiàn)類, 正是這些實(shí)現(xiàn)類形成責(zé)任鏈, 組成了二級緩存云石。
可以看出, 最上層是SyncronizedCache, 持有了一個名為delegate的LoggingCache類型對象, 以此類推, 直到鏈條上的最后一個Cache的實(shí)現(xiàn)類---PerpetualCache。而PerpetualCache本身持有了一個HashMap, 這才是二級緩存數(shù)據(jù)的真正存放地(緩存區(qū))汹忠。
以查詢?yōu)槔芟酰谡{(diào)用二級緩存的getObject()方法的時候, 就會從鏈條的起始端, 比如SynchronizedCache, 開始調(diào)用SynchronizedCache的getObject()方法, 在getObject()方法里面, 每個實(shí)現(xiàn)類都有兩部分的事情要做, 一個是完成自己特有的職能, 另一個是調(diào)用鏈條上的下一個Cache實(shí)現(xiàn)類的getObject()方法, 直到鏈條的尾端, 比如PerpetualCache。調(diào)用鏈雖然復(fù)雜, 但是每個實(shí)現(xiàn)類都是完成自己特有的附加功能, 而最終真正完成數(shù)據(jù)存儲工作的只有PerpetualCache這個類宽菜。
先來看下PerpetualCache這個類的源碼奖地, 在這個類中的getObject方法, 僅僅是從map中取出數(shù)據(jù)
public class PerpetualCache implements Cache:
private Map<Object, Object> cache = new HashMap<>();
@Overridepublic Object getObject(Object key) {
return cache.get(key);
}
而鏈條上的其他的Cache實(shí)現(xiàn)類是不是按照之前介紹的那樣, 做自己的功能并調(diào)用自己持有的鏈條上的下一個實(shí)現(xiàn)類的方法呢, 我們也可以以幾個實(shí)現(xiàn)類的源碼為例來論證赋焕。比如: SynchronizedCache(負(fù)責(zé)線程安全)和 LoggingCache(負(fù)責(zé)命中率統(tǒng)計)参歹。
在查看SynchronizedCache類的源碼的時候, 不要忽略getObject方法上的synchronized關(guān)鍵字,這個方法在負(fù)責(zé)線程安全的問題的后, 便調(diào)用了責(zé)任鏈上的下一個對象的getObject()方法隆判。
public class SynchronizedCache implements Cache:private final Cache delegate;@Overridepublic synchronized Object getObject(Object key) { // 注意:看這里H印!委派給下一個緩存實(shí)現(xiàn)類執(zhí)行g(shù)etObject()方法 return delegate.getObject(key);}
LoggingCache的getObject方法中侨嘀,除了調(diào)用鏈條上的下一個對象的方法外臭挽,還會統(tǒng)計請求的次數(shù)和命中的次數(shù),以此計算打印命中率咬腕。
public class LoggingCache implements Cache:private final Cache delegate;@Overridepublic Object getObject(Object key) requests++; //請求次數(shù) // 注意:看這里;斗濉! 委派給下一個緩存實(shí)現(xiàn)類執(zhí)行g(shù)etObject()方法 final Object value = delegate.getObject(key); if (value != null) { hits++; // 命中次數(shù) } if (log.isDebugEnabled()) { log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio()); } return value;}
NO.3 |事務(wù)緩存管理器
1.結(jié)構(gòu)
我們都知道一個會話中的事務(wù)在未提交之前, 其他會話是不允許讀到它未提交的數(shù)據(jù)的。在未加入二級緩存之前, 會話之間的都是如下圖所示的樣子, 各自為政, 互不干擾纽帖。
通過上一篇的學(xué)習(xí)可以了解到, 二級緩存是可以跨會話的宠漩。那么這里我們要思考一下, 如果我們加入了二級緩存, 并且按照緩存的一貫思路(進(jìn)行查詢操作的時候先查緩存, 如果緩存中沒有命中即查詢數(shù)據(jù)庫, 并且把查到的結(jié)果緩存到二級緩存中)來做, 會不會破壞原本的隔離性, 產(chǎn)生臟讀? 來看下面一張圖。
會話1首先進(jìn)行了修改操作, 然后進(jìn)行了查詢操作, 并且查詢后就把查到的結(jié)果放入緩存中, 而此時會話2也進(jìn)行了查詢操作, 就會查到緩存中的結(jié)果直接返回, 尷尬的是會話1最終沒有提交事務(wù), 選擇了回滾懊直。這樣就造成了會話2讀到的數(shù)據(jù)不準(zhǔn)確, 讀到了會話1未提交的數(shù)據(jù), 產(chǎn)生了臟讀扒吁。
所以Mybatis的二級緩存在設(shè)計時針對這樣的情況, 引入了事務(wù)緩存管理器。在事務(wù)緩存管理器中, 維護(hù)了一個本地暫存區(qū)(會話范圍內(nèi)可見), 本地暫存區(qū)又指向真正的緩存區(qū)(跨會話)室囊。在進(jìn)行查詢操作的時候, 會到緩存區(qū)中查看是否命中雕崩。如果沒有命中, 查詢數(shù)據(jù)庫得到數(shù)據(jù)后, 僅僅把查詢的結(jié)果放入暫存區(qū), 在提交事務(wù)的時候才要把暫存區(qū)中的數(shù)據(jù)刷新到緩存區(qū)。如果發(fā)生了回滾, 則清空本地暫存區(qū)緩存的數(shù)據(jù), 不會刷新到緩存區(qū), 這樣一來就避免了臟讀的產(chǎn)生融撞。
接下來我們先來通過部分源碼了解一下事務(wù)管理器的結(jié)構(gòu):
從以下代碼可以看出每個CachingExecutor對應(yīng)一個事務(wù)緩存管理器, 通過前面的學(xué)習(xí)我們知道, 每個會話中持有一個CachingExecutor(緩存執(zhí)行器)盼铁。所以每個會話都有自己單獨(dú)的事務(wù)緩存管理器。
CachingExecutor:
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
從以下代碼我們得知, 在事務(wù)緩存管理器中維護(hù)了一個HashMap, 這個HashMap便是暫存區(qū)的集合, 而且這個map的key是cache(緩存區(qū)), 所以每一個緩存區(qū)都有對應(yīng)的暫存區(qū)(TransactionalCache), 放在map中作為鍵值對被事務(wù)緩存管理器所維護(hù), 因?yàn)槊總€會話都有自己單獨(dú)的事務(wù)緩存管理器, 作為管理器屬性集合中的一個對象---暫存區(qū)也只是會話可見的尝偎。
TransactionalCacheManager:private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
接下來看一下代表著暫存區(qū)的TransactionalCache, 可以看見其中也維護(hù)了一個Map, 這個map是暫存區(qū)真正用來暫存數(shù)據(jù)的地方, 而delegate屬性, 代表的便是真正的緩存區(qū)(剛剛介紹過的, Cache的實(shí)現(xiàn)類組成的責(zé)任鏈, 完成了緩存區(qū)的維護(hù)), 有了與緩存區(qū)之間的關(guān)聯(lián), 在提交事務(wù)的時候, 就可以方便的把暫存區(qū)的數(shù)據(jù)刷新到緩存區(qū)了捉貌。
public class TransactionalCache implements Cache :private final Cache delegate;//指向緩存區(qū)private boolean clearOnCommit;private final Map<Object, Object> entriesToAddOnCommit;//暫存區(qū)
介紹完事務(wù)管理器, 暫存區(qū), 緩存區(qū)之間的結(jié)構(gòu)關(guān)系, 我們來通過源碼看下二級緩存進(jìn)行查詢和更新的過程。
2.查詢
從之前文章的學(xué)習(xí)我們已經(jīng)知道, 如果使用到二級緩存, 在查詢時, 會調(diào)用二級緩存的query方法冬念。這里主要看其中的tcm.getObject(cache, key)和tcm.putObject(cache, key, list)方法, 一個是通過事務(wù)緩存管理器取數(shù)據(jù)的方法, 一個是通過事務(wù)管理器放入數(shù)據(jù)的方法趁窃。
CachingExecutor:
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 獲取二級緩存
Cache cache = ms.getCache();
if (cache != null) {
// 刷新二級緩存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
// 從二級緩存中查詢數(shù)據(jù)
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
// 如果二級緩存中沒有查詢到數(shù)據(jù),則查詢數(shù)據(jù)庫
if (list == null) {
// 委托給BaseExecutor執(zhí)行
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 委托給BaseExecutor執(zhí)行
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
(1)tcm.getObject(cache, key)--->取出數(shù)據(jù)
在CachingExecutor的query()方法中, 先是調(diào)用了事務(wù)緩存管理器的getObject(cache, key)方法急前⌒崖剑可以看見TransactionalCacheManager在處理getObject()的時候先調(diào)用了getTransactionalCache(), 從map集合中取出當(dāng)前緩存區(qū)對應(yīng)的TransactionalCache(暫存區(qū)), 暫存區(qū)如果不存在, 則創(chuàng)建一個新的暫存區(qū)對象存入map, 然后調(diào)用獲得的TransactionalCache的getObject()方法。
TransactionalCacheManager:
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
private TransactionalCache getTransactionalCache(Cache cache) {
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
在TransactionalCache的getObject()方法中, 直接調(diào)用了其指向的緩存區(qū)的getObject()方法, 說明二級緩存在獲取數(shù)據(jù)的時候會直接去緩存區(qū)(跨會話)取數(shù)據(jù)裆针。
而在clearOnCommit這個布爾值為true的時候, 即使緩存區(qū)命中數(shù)據(jù)也只能返回null, 這是因?yàn)? 只有在有更新操作且未提交的時候clearOnCommit才是true, 這種狀態(tài)對于當(dāng)前會話當(dāng)前事務(wù)來說, 緩存區(qū)的數(shù)據(jù)已經(jīng)不準(zhǔn)確了, 所以最好的選擇是重新查詢數(shù)據(jù)庫刨摩。
public class TransactionalCache implements Cache :
private final Cache delegate;//指向緩存區(qū)(鏈條式的Cache實(shí)現(xiàn)類)
private boolean clearOnCommit;//執(zhí)行更新后clearOnCommit將變?yōu)閠rue
private final Map<Object, Object> entriesToAddOnCommit;//本地暫存//獲取緩存數(shù)據(jù), 從緩存區(qū)去查詢
@Override
public Object getObject(Object key) {
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
if (clearOnCommit) {//如果更新了數(shù)據(jù), 緩存區(qū)就算有數(shù)據(jù)也要返回空, 要去數(shù)據(jù)庫中取數(shù)據(jù)
return null;
} else {
return object;
}
}
(2)tcm.putObject(cache, key, list)--->放入數(shù)據(jù)
在query()方法中, 沒有從緩存區(qū)中取到數(shù)據(jù), 而重新查詢了數(shù)據(jù)的情況下, 就要調(diào)用tcm.putObject(), 通過事務(wù)管理器設(shè)置數(shù)據(jù)到緩存。與getObject()一樣, TransactionalCacheManager的putObject()方法也要先調(diào)用getTransactionalCache()獲得TransactionalCache(暫存區(qū)), 然后調(diào)用TransactionalCache的putObject()方法世吨。
TransactionalCacheManager:
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
private TransactionalCache getTransactionalCache(Cache cache) {
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
如果我們繼續(xù)查看, 就會發(fā)現(xiàn)在TransactionalCache的putObject()方法中, 數(shù)據(jù)僅被存到了暫存區(qū)中澡刹。
public class TransactionalCache implements Cache:
@Overridepublic void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object); // 存數(shù)據(jù), 存到暫存區(qū)
}
3.提交
在提交的方法中, 我們會把暫存區(qū)中的所有內(nèi)容刷新到緩存區(qū)中。
在我們調(diào)用sqlSession.commit()方法的時候, 也會調(diào)用當(dāng)前會話持有的緩存執(zhí)行器的commit()方法, 緩存執(zhí)行器會執(zhí)行事務(wù)緩存管理器的commit()方法耘婚“战剑看一下事務(wù)緩存管理器的提交的源碼, 在事務(wù)緩存管理器的commit()方法中, 會調(diào)用事務(wù)緩存管理器所有暫存區(qū)(TransactionalCache)的commit()方法。
TransactionalCacheManager:
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
在TransactionalCache的commit()方法中, 如果有未提交的更新操作(clearOnCommit為true), 則要清空緩存區(qū), 因?yàn)楦潞? 緩存區(qū)的數(shù)據(jù)便是不準(zhǔn)確的了沐祷。隨后調(diào)用flushPendingEntries()和reset()兩個方法, flushPendingEntries()方法負(fù)責(zé)把所有暫存區(qū)的內(nèi)容刷新到緩存中嚷闭。而reset()方法則負(fù)責(zé)把本地暫存區(qū)清空, 同時把clearOnCommit 置為false。
public class TransactionalCache implements Cache:
private final Cache delegate;//指向緩存區(qū)(鏈條式的Cache實(shí)現(xiàn)類)
private boolean clearOnCommit;//執(zhí)行更新后clearOnCommit將變?yōu)閠rue
private final Map<Object, Object> entriesToAddOnCommit;//本地暫存
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
private void reset() {
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
4.更新
在緩存執(zhí)行器調(diào)用更新操作的時候, 會調(diào)用flushCacheIfRequired(), 這個方法中會先判斷ms.isFlushCacheRequired(), 為true并且二級緩存存在就會執(zhí)行事務(wù)緩存執(zhí)行器的clear()方法, 而isFlushCacheRequired()就是從標(biāo)簽里面取到的flushCache的值赖临。而增刪改操作的flushCache屬性默認(rèn)為true胞锰。所以進(jìn)行更新的時候, 也會調(diào)用事務(wù)緩存管理器的clear方法。
public class CachingExecutor implements Executor:
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
在TransactionalCacheManager 的clear方法中兢榨。依然是先獲取暫存區(qū), 并調(diào)用暫存區(qū)的clear()方法嗅榕。
TransactionalCacheManager:
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
TransactionalCache的clear()方法中, clearOnCommit屬性被置為了true, 并清空了暫存區(qū)顺饮。清空暫存區(qū)不難理解, 因?yàn)槿绻嬖诟虏僮? 則暫存區(qū)中暫存起來的數(shù)據(jù)則有可能不再準(zhǔn)確了。并且緩存區(qū)也定然出現(xiàn)了不一致的情況, 所以在TransactionalCache的commit方法中, 會去判斷clearOnCommit是否為true(即是否進(jìn)行過更新操作), 如果是, 緩存區(qū)的數(shù)據(jù)也會被clear()掉凌那。而在清除執(zhí)行完成后, reset()方法中會把clearOnCommit重新置為false兼雄。
NO.4 |總結(jié)
Mybatis使用了裝飾者+責(zé)任鏈(變種)的模式構(gòu)建了二級緩存的組件, 每一個功能都有相應(yīng)的Cache實(shí)現(xiàn)類來完成, 同時這些實(shí)現(xiàn)類也會調(diào)用自己持有的Cache實(shí)現(xiàn)類, 完成責(zé)任鏈。最終被調(diào)用的類是PerpetualCache ,它就是最終負(fù)責(zé)數(shù)據(jù)存儲的類案怯。
而為了解決二級緩存跨會話使用可能引起的臟讀問題, mybatis引入了事務(wù)緩存管理器, 每一個會話持有一個事務(wù)緩存管理器, 每個事務(wù)緩存管理器維護(hù)著多個緩存區(qū)(每個namespace都有對應(yīng)的緩存區(qū))對應(yīng)的暫存區(qū), 暫存區(qū)中維護(hù)本地暫存數(shù)據(jù), 并指向它所屬的緩存區(qū)君旦。
通過事務(wù)緩存管理器查詢的時候, 直接去查緩存區(qū), 但是如果沒有命中, 重新查詢出的數(shù)據(jù)僅放入暫存區(qū), 直到進(jìn)行提交, 才把數(shù)據(jù)刷新到緩存區(qū)澎办。這是為了防止其他會話查到當(dāng)前會話中的事務(wù)未提交的數(shù)據(jù)嘲碱。而在執(zhí)行更新操作的時候, 會先清空對應(yīng)的暫存區(qū)數(shù)據(jù), 在提交事務(wù)的時候, 也會把對應(yīng)的緩存區(qū)數(shù)據(jù)清空。
結(jié)合我們之前講的兩篇文章, 所有關(guān)于mybatis多級緩存的事情就交代清楚了局蚀。這里除了編程知識分享麦锯,同時也是我成長腳印的記錄, 期待與您一起學(xué)習(xí)和進(jìn)步琅绅, 長按下方二維碼關(guān)注公眾號: 程序媛swag扶欣。如果您覺得這篇文章幫助到了您, 請幫忙點(diǎn)個在看 !
</object,>