前言
🍊作者簡介: 不肯過江東丶,一個來自二線城市的程式員,緻力于用“猥瑣”辦法解決繁瑣問題,讓複雜的問題變得通俗易懂。
🍊支援作者: 點贊👍、關注💖、留言💌~
在計算機的世界中,緩存無處不在,作業系統有作業系統的緩存,資料庫也會有資料庫的緩存,我們還可以利用中間件(例如 Redis)來充當緩存。 MyBatis 作為一款優秀的 ORM 架構,也用到了緩存,那麼今天咱們就一起來聊一聊 Mybatis 的一級緩存和二級緩存。
Mybatis 的一級緩存
首先我們先來看一張圖檔👇
我們在開發項目的過程中,如果我們開啟了 Mybatis 的 SQL 語句列印,我們就會經常看到這句話:Creating a new SqlSession,其實這就是我們常說的 Mybatis 的一級緩存。
Mybatis 的一級緩存也就是在執行一次 SQL 查詢或者 SQL 更新之後,這條 SQL 語句并不會消失,而是被 MyBatis 緩存起來,當再次執行相同SQL語句的時候,就會直接從緩存中進行提取,而不是再次執行SQL指令。一級緩存又被稱為 SqlSession 級别的緩存,在操作資料庫時需要構造 SqlSession 對象,在對象中有一個資料結構(HashMap)用于存儲緩存資料。不同的 SqlSession 之間的緩存資料區(HashMap)是互相不影響的。
在我們的應用系統的運作期間,我們可能在一次資料庫會話中,執行多次查詢條件相同的 SQL 語句,那麼針對此情況,你來設計的話你會如何考慮呢?沒錯,就是加緩存,MyBatis 也是這樣去處理的,如果是相同的 SQL 語句,會優先命中一級緩存,避免直接對資料庫進行查詢,造成資料庫的壓力,以提高性能。具體執行過程如下圖所示👇
SqlSession 是一個接口,提供了一些 CRUD 的方法,而 SqlSession 的預設實作類是 DefaultSqlSession,DefaultSqlSession 類持有 Executor 接口對象,而 Executor 的預設實作是 BaseExecutor 對象,每個 BaseExecutor 對象都有一個 PerpetualCache 緩存,也就是上圖的 Local Cache。當使用者發起查詢時,MyBatis 根據目前執行的語句生成 MappedStatement,在 Local Cache 進行查詢,如果緩存命中的話,直接傳回結果給使用者,如果緩存沒有命中的話,查詢資料庫,結果寫入 Local Cache,最後傳回結果給使用者。這時候可能有小夥伴要說了:我還在控制台上見到了“Closing non transactional SqlSession ”這句話,那我每次建立的 SqlSession 到最後都被關閉了,那我還緩存個毛線了 😥
事請當然不會像我們想象的那樣,我們繼續往下看👇
🍊 getSqlSession 源碼
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
Assert.notNull(sessionFactory, "No SqlSessionFactory specified");
Assert.notNull(executorType, "No ExecutorType specified");
// 如果目前我們開啟了事物,那就從 ThreadLocal 裡面擷取 session
SqlSessionHolder holder = (SqlSessionHolder)TransactionSynchronizationManager.getResource(sessionFactory);
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
} else {
LOGGER.debug(() -> {
return "Creating a new SqlSession";
});
// 沒有擷取到 session,建立一個 session
session = sessionFactory.openSession(executorType);
// 如果目前開啟了事物,就把這個session注冊到目前線程的 ThreadLocal 裡面去
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
}
🍊 closeSqlSession 源碼
public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) {
Assert.notNull(session, "No SqlSession specified");
Assert.notNull(sessionFactory, "No SqlSessionFactory specified");
SqlSessionHolder holder = (SqlSessionHolder)TransactionSynchronizationManager.getResource(sessionFactory);
if (holder != null && holder.getSqlSession() == session) {
LOGGER.debug(() -> {
return "Releasing transactional SqlSession [" + session + "]";
});
holder.released();
} else {
LOGGER.debug(() -> {
return "Closing non transactional SqlSession [" + session + "]";
});
session.close();
}
}
我們使用官方的解釋來說 closeSqlSession 方法就是:檢查作為參數傳遞的 SqlSession 是否由 Spring TransactionSynchronizationManage 管理。如果不是,則關閉它,否則它隻更新引用計數器,并在托管事務結束時讓 Spring 調用關閉回調。簡單點來說就是“如果我們方法是開啟事物的,則目前事物内是擷取的同一個 sqlSession,否則每次都是擷取不同的 sqlSession”,是以我們也并不需要擔心無法擷取到對應的緩存。這時候有些小夥伴可能又有疑問了:Mybatis 的一級緩存什麼情況下會過期呢?各位稍安勿躁,我們接着往下看👇
我們一開始就說了,Mybatis 的一級緩存是存在 sqlSession 裡面的,毫無疑問當 sqlSession 被清空或者關閉的時候緩存就沒了(在不開啟事物的情況下,每次都會關閉 sqlSession);除此之外,在執行 insert、update、delete 的時候也會清空緩存。我們通過源碼可以發現 sqlSession 的 insert 和 delete 方法的本質都是執行的 update 方法 👇
我們再來看看 update 的源碼👇
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
this.clearLocalCache();
return this.doUpdate(ms, parameter);
}
}
執行到 this.clearLocalCache(); 的時候,緩存就已經被清理掉了,也就是說此時 Mybatis 的一級緩存就過期了🧐
我們說了這麼多,相信各位小夥伴也了解到了 MyBatis 一級緩存的相關内容,不過 MyBatis 的一級緩存最大的共享範圍就是一個 SqlSession 内部,那麼如果多個 SqlSession 需要共享緩存該怎麼辦呢?沒錯!這時候就需要 MyBatis 的二級緩存登場了 😎
Mybatis 的二級緩存
如果需要多個 SqlSession 共享緩存,則需要我們開啟二級緩存。開啟二級緩存後,會使用 CachingExecutor 裝飾 Executor,進入一級緩存的查詢流程前,先在 CachingExecutor 進行二級緩存的查詢,具體的工作流程如下所示👇
當二級緩存開啟後,同一個命名空間(namespace)所有的操作語句,都影響着一個共同的 cache,也就是二級緩存被多個 SqlSession 共享,我們可以将其了解成一個全局變量。當開啟二級緩存後,資料的查詢執行流程就變為了:二級緩存 → 一級緩存 → 資料庫。關于查詢的執行流程,我們可以通過源碼加以佐證,在 CachingExecutor 檔案下的 query 方法很容易就看到了,如果開啟二級緩存那就走二級緩存,否則就走一級緩存,如下圖所示👇
Mybatis 的二級緩存不像一級緩存預設就是開啟的,我們需要在對應的 Mapper 檔案裡面加上 cache 标簽,手動開啟 Mybatis 的二級緩存👇
我們可以看到 cache 标簽有多個屬性,我們先來一起看一下這些屬性都分别代表了什麼含義:
- type:指定自定義緩存的全類名(一般我們可以使用該 Mapper 檔案的全路徑作為 type 值)。
- readOnly:是否隻讀。true 隻讀,MyBatis 認為所有從緩存中擷取資料的操作都是隻讀操作,不會修改資料,同時 MyBatis 為了加快擷取資料的速度,直接就會将資料在緩存中的引用交給使用者,雖然速度快變快了,但是安全性卻降低了。如果不設定該屬性的話,則預設為讀寫。
- size:緩存存放多少個元素。
- blocking:若緩存中找不到對應的key,是否會一直阻塞(blocking),直到有對應的資料進入緩存。
- flushinterval:緩存重新整理間隔,緩存多長時間重新整理一次,預設不重新整理。
- eviction: 緩存回收政策,回收政策共有以下四種
LRU:最近最少回收,移除最長時間不被使用的對象(預設值)
FIFO:先進先出,按照緩存進入的順序來移除它們
SOFT:軟引用,移除基于垃圾回收器狀态和軟引用規則的對象
WEAK:弱引用,更積極的移除基于垃圾收集器和弱引用規則的對象
🍊 解析 cache 标簽的 cacheElement 方法源碼
private void cacheElement(XNode context) {
if (context != null) {
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = this.typeAliasRegistry.resolveAlias(type);
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = this.typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
this.builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
不知道各位小夥伴知不知道 Mybatis 的二級緩存應用了什麼設計模式呢?其中最明顯的就是應用了裝飾器模式~
public Cache build() {
// 設定預設的緩存實作類和預設的裝飾器(PerpetualCache 和 LruCache)
this.setDefaultImplementations();
// 建立基本的緩存
Cache cache = this.newBaseCacheInstance(this.implementation, this.id);
// 設定自定義的參數
this.setCacheProperties((Cache)cache);
// 如果是PerpetualCache 的緩存,将進一步進行處理
if (PerpetualCache.class.equals(cache.getClass())) {
Iterator var2 = this.decorators.iterator();
while(var2.hasNext()) {
Class<? extends Cache> decorator = (Class)var2.next();
// 進行最基本的裝飾
cache = this.newCacheDecoratorInstance(decorator, (Cache)cache);
// 設定自定義的參數
this.setCacheProperties((Cache)cache);
}
// 建立标準的緩存,也就是根據配置來進行不同的裝飾
cache = this.setStandardDecorators((Cache)cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
// 如果是自定義的緩存實作,這裡隻進行日志裝飾器
cache = new LoggingCache((Cache)cache);
}
return (Cache)cache;
}
既然是裝飾器模式,那肯定不止一兩種裝飾器😄 Mybatis 的源碼中一共提供了多種裝飾器,比如LruCache、ScheduledCache、LoggingCache 等等,我們通過類名就大概能猜到他們的作用👇
這裡有一點是需要注意的:其實他們并不是 cache 的實作類,真正的實作類隻有 PerpetualCache ,紅框裡面的類都是對 PerpetualCache 的包裝。
我們了解了緩存裝飾器,我們再來看看設定标準裝飾器的源碼👇
private Cache setStandardDecorators(Cache cache) {
try {
// 擷取目前 cache的參數
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (this.size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", this.size);
}
// 如果設定了緩存重新整理時間,就進行ScheduledCache 裝飾
if (this.clearInterval != null) {
cache = new ScheduledCache((Cache)cache);
((ScheduledCache)cache).setClearInterval(this.clearInterval);
}
// 如果緩存可讀可寫,就需要進行序列化 預設就是 true,這也是為什麼我們的二級緩存的需要實作序列化(即對應實體類必須實作序列化接口)
if (this.readWrite) {
cache = new SerializedCache((Cache)cache);
}
// 預設都裝飾 日志和同步
Cache cache = new LoggingCache((Cache)cache);
cache = new SynchronizedCache(cache);
// 如果開啟了阻塞就裝配阻塞
if (this.blocking) {
cache = new BlockingCache((Cache)cache);
}
return (Cache)cache;
} catch (Exception var3) {
throw new CacheException("Error building standard cache decorators. Cause: " + var3, var3);
}
}
看完這塊代碼,心理就是一個字:爽!! 能把裝飾器模式用的如此精妙,也真是沒誰了。該說不說,隻要能把這塊源碼了解通透,那裝飾器模式就真的完全掌握了😉
通過上面的源碼,我們知道 Mybatis 的二級緩存預設就是可讀可寫的緩存,它會用 SynchronizedCache 進行裝飾,我們來看來SynchronizedCache 的 putObject 方法👇
public void putObject(Object key, Object object) {
if (object != null && !(object instanceof Serializable)) {
throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object);
} else {
this.delegate.putObject(key, this.serialize((Serializable)object));
}
}
這也就是為什麼二級緩存的實體一定要實作序列化接口的原因了,當然如果将二級緩存設定為隻讀的緩存,那麼也就不需要實作序列化接口了。
最後我們回歸實際,在分布式架構盛行的當下,我們該如何選擇使用哪種緩存呢?其實答案也很簡單:除非對性能要求特别高,否則一級緩存和二級緩存都不建議使用,Mybatis 的一級緩存和二級緩存都是基于本地的,分布式環境下必然會出現髒讀。
雖然 Mybatis 的二級緩存可以通過實作 Cache 接口集中管理緩存,避免出現髒讀的情況,但是有一定的開發成本,并且在多表查詢時,使用不當極有可能會出現髒資料~
小結
本人經驗有限,有些地方可能講的沒有特别到位,如果您在閱讀的時候想到了什麼問題,歡迎在評論區留言,我們後續再一一探讨🙇
希望各位小夥伴動動自己可愛的小手,來一波點贊+關注 (✿◡‿◡) 讓更多小夥伴看到這篇文章~ 蟹蟹呦(●’◡’●)
如果文章中有錯誤,歡迎大家留言指正;若您有更好、更獨到的了解,歡迎您在留言區留下您的寶貴想法。
你在被打擊時,記起你的珍貴,抵抗惡意;
你在迷茫時,堅信你的珍貴,抛開蜚語;
愛你所愛 行你所行 聽從你心 無問東西