當前位置: 妍妍網 > 碼農

美團二面:為什麽不推薦使用 MyBatis 二級緩存?大部份人都答不上來!

2024-02-23碼農

點選「 IT碼徒 」, 關註,置頂 公眾號

每日技術幹貨,第一時間送達!

為了增加查詢的效能,MyBatis 提供了二級緩存架構,分為一級緩存和二級緩存。

這兩級緩存最大的區別就是:一級緩存是會話級別的,只要出了這個 SqlSession,緩存就沒用了。而二級緩存可以跨會話,多個會話可以使用相同的緩存!

一級緩存使用簡單,預設就開啟。二級緩存需要手動開啟,相對復雜,而且要註意的事項也多,否則可能有隱患。

一級緩存

套用場景

訂單表與會員表是存在一對多的關系,為了盡可能減少 join 查詢,進行了分階段查詢。即先查詢出訂單表,再根據 member_id 欄位查詢出會員表,最後進行數據整合。而如果訂單表中存在重復的 member_id,就會出現很多重復查詢。

針對這種情況,MyBatis 透過一級緩存來解決:在同一次查詢會話(SqlSession)中如果出現相同的語句及參數,就會從緩存中取出,不再走資料庫查詢。

一級緩存只能作用於查詢會話中,所以也叫做會話緩存。

生效的條件

一級緩存要生效,必須滿足以下條件條件:

  • 必須是相同的會話

  • 必須是同一個 mapper,即同一個 namespace

  • 必須是相同的 statement,即同一個 mapper 中的同一個方法

  • 必須是相同的 SQL 和參數

  • 查詢語句中間沒有執行 session.clearCache() 方法

  • 查詢語句中間沒有執行 insert/update/delete 方法(無論變動記錄是否與緩存數據有無關系)

  • 與 SpringBoot 整合時一級緩存不生效原因

    因為一級緩存是會話級別的,要生效的話,必須要在同一個 SqlSession 中。但是與 SpringBoot 整合的 MyBatis,預設每次執行 SQL 語句時,都會建立一個新的 SqlSession!所以一級緩存才沒有生效。

    當呼叫 mapper 的方法時,最終會執行到 SqlSessionUtils 的 getSqlSession 方法,在這個方法中會嘗試在事務管理器中獲取 SqlSession,如果沒有開啟事務,那麽就會 new 一個 DefaultSqlSession。

    所以說,即便在同一個方法中,透過同一個 mapper 連續呼叫兩次相同的查詢方法,也不會觸發一級緩存。

    解決與 SpringBoot 整合時一級緩存不生效問題

    在上面的程式碼中也看到了,MyBatis 在查詢時,會先從事務管理器中嘗試獲取 SqlSession,取不到才會去建立新的 SqlSession。所以可以猜測只要將方法開啟事務,那麽一級緩存就會生效。

    加上 @Transactional 註解,看下效果:

    沒錯,的確生效了。在程式碼中可以看到,從事務管理器中,獲取到了 SqlSession:

    再看看源碼中是什麽時候將 SqlSession 設定到事務管理器中的。

    SqlSessionUtils 中,在獲取到 SqlSession 後,會呼叫 registerSessionHolder 方法註冊 SessionHolder 到事務管理器:

    具體是在 TransactionSynchronizationManager 的 bindResource 方法中操作的,將 SessionHolder 保存到執行緒本地變量 (ThreadLocal) resources 中,這是每個執行緒獨享的:

    然後在下次查詢時,就可以從這裏取出此 SqlSession,使用同一個 SqlSession 查詢,一級緩存就生效了。

    所以基本原理就是:如果當前執行緒存在事物,並且存在相關會話,就從 ThreadLocal 中取出。如果沒有事務,就重新建立一個 SqlSession 並儲存到 ThreadLocal 當中,共下次查詢使用。

    至於緩存查詢數據的地方,是在 BaseExecutor 中的 queryFromDatabase 方法中。執行 doQuery 從資料庫中查詢數據後,會立馬緩存到 localCache(PerpetualCache型別) 中:

    二級緩存

    套用場景

    業務系統中存在很多的靜態數據如,字典表、選單表、許可權表等,這些數據的特性是不會輕易修改但又是查詢的熱點數據。

    一級緩存針對的是同一個會話當中相同 SQL,並不適合熱點數據的緩存場景。

    為了解決這個問題引入了二級緩存,它脫離於會話之外,多個會話可以使用相同的緩存。

    看一個例子:

    @RestController
    @RequestMapping("item")
    public class ItemController {
    @Autowired
    private ItemMapper itemMapper;
    @GetMapping("/{id}")
    public void getById(@PathVariable("id") Long id) {
    System.out.println("==================== begin ====================");
    Item item = itemMapper.selectById(id);
    System.out.println(JSON.toJSONString(item));
    }
    }

    當發送兩次 get 請求時(兩個不同的會話),透過日誌可以發現第二次查詢使用的是緩存。

    開啟的方法

    二級緩存需要手動來開啟,MyBatis 預設沒有開啟二級緩存。

    1)在 yaml 中配置 cache-enabled 為 true

    mybatis-plus:
    configuration:
    cache-enabled: true

    2)Mapper 介面上添加 @CacheNamespace 註解

    3)實體類實作 Serializable 介面

    生效的條件

  • 當會話送出或關閉之後才會填充二級緩存

  • 必須是同一個 mapper,即同一個名稱空間

  • 必須是相同的 statement,即同一個 mapper 中的同一個方法

  • 必須是相同的 SQL 語句和參數

  • 如果 readWrite=true(預設就是true),實體對像必須實作 Serializable 介面

  • 緩存清除條件

  • 只有修改會話送出之後,才會執行清空操作

  • xml 中配置的 update 不能清空 @CacheNamespace 中的緩存數據

  • 任何一種增刪改操作都會清空整個 namespace 中的緩存

  • 源碼中是如何填充二級緩存的?

    在生效條件中提到了,二級緩存必須要在會話送出或關閉之後,才能生效!

    在查詢到結果後,會呼叫 SqlSession 的 commit 方法進行送出(如果開啟事務的話,送出 SqlSession 走的不是這裏了,但最終填充二級緩存的地方是一樣的):

    在此方法中,最終會呼叫到 TransactionalCache 的 flushPendingEntries 方法中填充二級緩存:

    SpringBoot 整合 MyBatis 的話,如果沒有開啟事務,每次執行查詢,都會建立新的 SqlSession,所以即使是在同一個方法中進行查詢操作,那也是跨會話的。

    查詢時如何使用二級緩存?

    在查詢的時候,最終會呼叫 MybatisCachingExecutor 的 query 方法,裏面會從 TransactionalCacheManager 中嘗試根據 key 獲取二級緩存的內容。

    可以看到,這個 key 很長,由 mapper、呼叫的查詢方法、SQL 等資訊拼接而成,這也是為什麽想要二級緩存生效,必須滿足前面所說的條件。

    如果能在二級緩存中查詢到,就直接返回了,不需要存取資料庫。

    具體的呼叫層數實在太多,用到了裝飾者模式,最終是在 PerpetualCache 中獲取緩存的:

    打印日誌是在 LoggingCache 中:

    為什麽 MyBatis 預設不開啟二級緩存?

    答案就是,不推薦使用二級緩存!

    二級緩存雖然能帶來一定的好處,但是有很大的隱藏危害!

    它的緩存是以 namespace(mapper) 為單位的,不同 namespace 下的操作互不影響。且 insert/update/delete 操作會清空所在 namespace 下的全部緩存。

    那麽問題就出來了,假設現在有 ItemMapper 以及 XxxMapper,在 XxxMapper 中做了表關聯查詢,且做了二級緩存。此時在 ItemMapper 中將 item 資訊給刪了,由於不同 namespace 下的操作互不影響,XxxMapper 的二級緩存不會變,那之後再次透過 XxxMapper 查詢的數據就不對了,非常危險。

    來看一個例子:

    @Mapper
    @Repository
    @CacheNamespace
    public interface XxxMapper {
    @Select("select i.id itemId,i.name itemName,p.amount,p.unit_price unitPrice " +
    "from item i JOIN payment p on i.id = p.item_id where i.id = #{id}")
    List<PaymentVO> getPaymentVO(Long id);
    }
    @Autowired
    private XxxMapper xxxMapper;
    @Test
    void test() {
     System.out.println("==================== 查詢PaymentVO ====================");
     List<PaymentVO> voList = xxxMapper.getPaymentVO(1L);
     System.out.println(JSON.toJSONString(voList.get(0)));
     System.out.println("==================== 更新item表的name ==================== ");
     Item item = itemMapper.selectById(1);
     item.setName("java並行編程");
     itemMapper.updateById(item);
     System.out.println("==================== 重新查詢PaymentVO ==================== ");
     List<PaymentVO> voList2 = xxxMapper.getPaymentVO(1L);
     System.out.println(JSON.toJSONString(voList2.get(0)));
    }


    上面的程式碼,test() 方法中前後兩次呼叫了 xxxMapper.getPaymentVO 方法,因為沒有加 @Transactional 註解,所以前後兩次查詢,是兩個不同的會話,第一次查詢完後,SqlSession 會自動 commit,所以二級緩存能夠生效;

    然後在中間進行了 Item 表的更新操作,修改了下名稱;

    由於 itemMapper 與 xxxMapper 不是同一個名稱空間,所以 itemMapper 執行的更新操作不會影響到 xxxMapper 的二級緩存;

    再次呼叫 xxxMapper.getPaymentVO,發現取出的值是走緩存的,itemName 還是老的。但實際上 itemName 在上面已經被改了!

    執行日誌如下:

    所以說,二級緩存的隱藏危害是比較大的,當有表關聯時,一個不註意就會出問題,不建議使用。

    來源:blog.csdn.net/xujingyiss/article/details/123481116

    END

    PS:防止找不到本篇文章,可以收藏點贊,方便翻閱尋找哦。

    往期推薦