天天看點

SpringBoot--解決@Transactional與@CacheEvict聯合使用導緻的緩存與資料庫的一緻性問題

簡介

說明

        本文介紹@Transactional與@CacheEvict聯合使用導緻的緩存與資料庫的一緻性問題的原因及解決方案。

注解的作用

        @Transactional:給目前方法添加事務支援,是通過 AOP 動态代理實作的,在方法執行完之後送出事務。

        @CacheEvict:在該方法執行完之後,清除 redis 中的緩存,也是使用 AOP 動态代理實作的。

問題引出

如果是@Transactional與@CacheEvict用在同一個方法上邊,是否有問題?

是否可以達到如下效果:先儲存到資料庫,送出資料庫事務,然後清除緩存?

SpringBoot--解決@Transactional與@CacheEvict聯合使用導緻的緩存與資料庫的一緻性問題

打斷點分析

給@CacheEvict打斷點

        執行清除緩存的是org.springframework.cache.Cache#evict方法,此處是使用 redis 作為緩存的提供者,是以在清除緩存時必然會調用 redis 緩存實作類的方法,即:org.springframework.data.redis.cache.RedisCache#evict。于是,在該方法處加一個斷點:

SpringBoot--解決@Transactional與@CacheEvict聯合使用導緻的緩存與資料庫的一緻性問題

給@Transactional打斷點

        對于 JDBC 事務而言,想要送出事務就要調用java.sql.Connection#commit方法。由于筆者此處使用的是 MySQL 資料庫,是以這裡對應的實作類為com.mysql.jdbc.ConnectionImpl#commit。于是,同樣在該方法加一個斷點:

SpringBoot--解決@Transactional與@CacheEvict聯合使用導緻的緩存與資料庫的一緻性問題

測試程式

        在執行 save 方法之前,通過調用 getById 方法已經将對應的資料緩存到了 redis 中。此時,Redis和資料庫中 countNumber 的值為 1。

SpringBoot--解決@Transactional與@CacheEvict聯合使用導緻的緩存與資料庫的一緻性問題

測試

調用save方法,id為1,countNumber為2。

  1. 首先命中了org.springframework.data.redis.cache.RedisCache#evict方法的斷點,執行完該方法之後,對應的Redis資料已被清除(countNumber為空)。此時資料庫裡countNumber仍然是1。
  2. 再向下運作,到達com.mysql.jdbc.ConnectionImpl#commit,執行事務送出,執行完後,資料庫裡countNumber變成2

我們希望先送出事務,然後更新緩存。而真正的執行順序是,先清除緩存,然後送出事務。

這樣存在的問題:資料庫和緩存資料不一緻

先清除緩存,然後在事務還沒有送出之前,程式就收到了使用者的請求,發現緩存中沒有資料,則去資料庫中擷取資料(事務還沒有送出則擷取到舊值),同時将擷取的資料添加到緩存中。此時會導緻資料庫和緩存資料不一緻。

解決方案

方案1:将事務單獨拿出為一個方法

SpringBoot--解決@Transactional與@CacheEvict聯合使用導緻的緩存與資料庫的一緻性問題

方案2:修改AOP執行順序

修改 AOP 的執行順序:改成先送出事務,再清除緩存。

方法如下:

@EnableCaching(order = Ordered.HIGHEST_PRECEDENCE)      

注意

優先級越高不是應該越先執行嗎?緩存 AOP 的優先級最高怎麼比事務送出 AOP 執行的要晚呢?

多個 advice 運作在同一個 join point 時,Spring AOP 遵循與 AspectJ 相同的優先級規則來确定建議執行的順序。可以通過實作org.springframework.core.Ordered接口或者使用@Order注解來控制其執行順序。優先級最高的 advice 首先“在入口”運作,從 join point“出來”時,優先級最高的 advice 将最後運作。

可以把 Spring AOP 想象成一個同心圓。被增強的原始方法在圓心,每一層 AOP 就是增加一個新的同心圓。同時,優先級最高的在最外層。方法被調用時,從最外層按照 AOP1、AOP2 的順序依次執行 around、before 方法,然後執行 method 方法,最後按照 AOP2、AOP1 的順序依次執行 after 方法。

SpringBoot--解決@Transactional與@CacheEvict聯合使用導緻的緩存與資料庫的一緻性問題

修改AOP順序的方法分析

斷點入口

SpringBoot--解決@Transactional與@CacheEvict聯合使用導緻的緩存與資料庫的一緻性問題

        @Transactional和@CacheEvict都是通過動态代理來實作的,在執行 save 方法處打一個斷點,命中斷點之後,點選Step Into,就可以進入到代理對象的執行方法内。 

攔截的方法

SpringBoot--解決@Transactional與@CacheEvict聯合使用導緻的緩存與資料庫的一緻性問題

        可以看到,執行 save 方法之前,被CglibAopProxy.DynamicAdvisedInterceptor#intercept方法所攔截了。(在 SpringBoot2.0 之後,SpringBoot 中 AOP 的預設實作被設定成了預設使用 CGLIB 來實作了。) 

所有的切面

SpringBoot--解決@Transactional與@CacheEvict聯合使用導緻的緩存與資料庫的一緻性問題

通過 debug 可以發現:advised.advisors是一個 List,List 中的兩個 Advisor 分别為:

  • org.springframework.cache.interceptor.BeanFactoryCacheOperationSourceAdvisor: advice org.springframework.cache.interceptor.CacheInterceptor@4b2e3e8f
  • org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor: advice org.springframework.transaction.interceptor.TransactionInterceptor@27a97e08

那怎麼修改 List 内元素的順序呢?

通過檢視BeanFactoryCacheOperationSourceAdvisor和BeanFactoryTransactionAttributeSourceAdvisor的源碼可知,這兩個類均繼承了org.springframework.aop.support.AbstractPointcutAdvisor,而AbstractPointcutAdvisor這個抽象類實作了org.springframework.core.Ordered接口。

猜想:那我們是不是可以通過修改 getOrder()方法的傳回值來影響 List 中的排序呢?

Order的設定方法

SpringBoot--解決@Transactional與@CacheEvict聯合使用導緻的緩存與資料庫的一緻性問題
SpringBoot--解決@Transactional與@CacheEvict聯合使用導緻的緩存與資料庫的一緻性問題

        以BeanFactoryTransactionAttributeSourceAdvisor為例,order 的值來自于AnnotationAttributes enableTx對象的某個屬性。

SpringBoot--解決@Transactional與@CacheEvict聯合使用導緻的緩存與資料庫的一緻性問題

        通過源碼可以發現,AnnotationAttributes enableTx的屬性全部都來自于@EnableTransactionManagement注解。

SpringBoot--解決@Transactional與@CacheEvict聯合使用導緻的緩存與資料庫的一緻性問題

其他網址