天天看點

大聰明教你學Java | 深入淺出聊 Mybatis 的一級緩存和二級緩存前言Mybatis 的一級緩存Mybatis 的二級緩存小結

前言

🍊作者簡介: 不肯過江東丶,一個來自二線城市的程式員,緻力于用“猥瑣”辦法解決繁瑣問題,讓複雜的問題變得通俗易懂。

🍊支援作者: 點贊👍、關注💖、留言💌~

在計算機的世界中,緩存無處不在,作業系統有作業系統的緩存,資料庫也會有資料庫的緩存,我們還可以利用中間件(例如 Redis)來充當緩存。 MyBatis 作為一款優秀的 ORM 架構,也用到了緩存,那麼今天咱們就一起來聊一聊 Mybatis 的一級緩存和二級緩存。

Mybatis 的一級緩存

首先我們先來看一張圖檔👇

大聰明教你學Java | 深入淺出聊 Mybatis 的一級緩存和二級緩存前言Mybatis 的一級緩存Mybatis 的二級緩存小結

我們在開發項目的過程中,如果我們開啟了 Mybatis 的 SQL 語句列印,我們就會經常看到這句話:Creating a new SqlSession,其實這就是我們常說的 Mybatis 的一級緩存。

Mybatis 的一級緩存也就是在執行一次 SQL 查詢或者 SQL 更新之後,這條 SQL 語句并不會消失,而是被 MyBatis 緩存起來,當再次執行相同SQL語句的時候,就會直接從緩存中進行提取,而不是再次執行SQL指令。一級緩存又被稱為 SqlSession 級别的緩存,在操作資料庫時需要構造 SqlSession 對象,在對象中有一個資料結構(HashMap)用于存儲緩存資料。不同的 SqlSession 之間的緩存資料區(HashMap)是互相不影響的。

在我們的應用系統的運作期間,我們可能在一次資料庫會話中,執行多次查詢條件相同的 SQL 語句,那麼針對此情況,你來設計的話你會如何考慮呢?沒錯,就是加緩存,MyBatis 也是這樣去處理的,如果是相同的 SQL 語句,會優先命中一級緩存,避免直接對資料庫進行查詢,造成資料庫的壓力,以提高性能。具體執行過程如下圖所示👇

大聰明教你學Java | 深入淺出聊 Mybatis 的一級緩存和二級緩存前言Mybatis 的一級緩存Mybatis 的二級緩存小結

SqlSession 是一個接口,提供了一些 CRUD 的方法,而 SqlSession 的預設實作類是 DefaultSqlSession,DefaultSqlSession 類持有 Executor 接口對象,而 Executor 的預設實作是 BaseExecutor 對象,每個 BaseExecutor 對象都有一個 PerpetualCache 緩存,也就是上圖的 Local Cache。當使用者發起查詢時,MyBatis 根據目前執行的語句生成 MappedStatement,在 Local Cache 進行查詢,如果緩存命中的話,直接傳回結果給使用者,如果緩存沒有命中的話,查詢資料庫,結果寫入 Local Cache,最後傳回結果給使用者。這時候可能有小夥伴要說了:我還在控制台上見到了“Closing non transactional SqlSession ”這句話,那我每次建立的 SqlSession 到最後都被關閉了,那我還緩存個毛線了 😥

大聰明教你學Java | 深入淺出聊 Mybatis 的一級緩存和二級緩存前言Mybatis 的一級緩存Mybatis 的二級緩存小結

事請當然不會像我們想象的那樣,我們繼續往下看👇

🍊 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 方法 👇

大聰明教你學Java | 深入淺出聊 Mybatis 的一級緩存和二級緩存前言Mybatis 的一級緩存Mybatis 的二級緩存小結
大聰明教你學Java | 深入淺出聊 Mybatis 的一級緩存和二級緩存前言Mybatis 的一級緩存Mybatis 的二級緩存小結
大聰明教你學Java | 深入淺出聊 Mybatis 的一級緩存和二級緩存前言Mybatis 的一級緩存Mybatis 的二級緩存小結

我們再來看看 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 進行二級緩存的查詢,具體的工作流程如下所示👇

大聰明教你學Java | 深入淺出聊 Mybatis 的一級緩存和二級緩存前言Mybatis 的一級緩存Mybatis 的二級緩存小結

當二級緩存開啟後,同一個命名空間(namespace)所有的操作語句,都影響着一個共同的 cache,也就是二級緩存被多個 SqlSession 共享,我們可以将其了解成一個全局變量。當開啟二級緩存後,資料的查詢執行流程就變為了:二級緩存 → 一級緩存 → 資料庫。關于查詢的執行流程,我們可以通過源碼加以佐證,在 CachingExecutor 檔案下的 query 方法很容易就看到了,如果開啟二級緩存那就走二級緩存,否則就走一級緩存,如下圖所示👇

大聰明教你學Java | 深入淺出聊 Mybatis 的一級緩存和二級緩存前言Mybatis 的一級緩存Mybatis 的二級緩存小結

Mybatis 的二級緩存不像一級緩存預設就是開啟的,我們需要在對應的 Mapper 檔案裡面加上 cache 标簽,手動開啟 Mybatis 的二級緩存👇

大聰明教你學Java | 深入淺出聊 Mybatis 的一級緩存和二級緩存前言Mybatis 的一級緩存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 等等,我們通過類名就大概能猜到他們的作用👇

大聰明教你學Java | 深入淺出聊 Mybatis 的一級緩存和二級緩存前言Mybatis 的一級緩存Mybatis 的二級緩存小結

這裡有一點是需要注意的:其實他們并不是 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 接口集中管理緩存,避免出現髒讀的情況,但是有一定的開發成本,并且在多表查詢時,使用不當極有可能會出現髒資料~

小結

本人經驗有限,有些地方可能講的沒有特别到位,如果您在閱讀的時候想到了什麼問題,歡迎在評論區留言,我們後續再一一探讨🙇‍

希望各位小夥伴動動自己可愛的小手,來一波點贊+關注 (✿◡‿◡) 讓更多小夥伴看到這篇文章~ 蟹蟹呦(●’◡’●)

如果文章中有錯誤,歡迎大家留言指正;若您有更好、更獨到的了解,歡迎您在留言區留下您的寶貴想法。

你在被打擊時,記起你的珍貴,抵抗惡意;

你在迷茫時,堅信你的珍貴,抛開蜚語;

愛你所愛 行你所行 聽從你心 無問東西