當@Transactional遇到@CacheEvict,你的代碼是不是有bug!

轉自:Coder小黑

作者:coder小黑

有bug嗎

如上圖所示,當@Transactional 遇到@CacheEvict,緩存放在 redis 中,這樣寫代碼會有什麼問題呢?你們的程序中是否寫着這樣的代碼呢?如果是,請你立刻修改!

思考 ????

首先,@Transactional是給當前方法添加事務支持,是通過 AOP 動態代理實現的,在方法執行完之後才提交事務。其次,@CacheEvict是在該方法執行完之後,清除 redis 中的緩存,也是使用 AOP 動態代理實現的。

那麼,上述方法想表達語義應該是:先保存對象,提交事務,然後清除緩存。但是,這樣寫真的能達到這個語義嗎?

Debug 尋找真相 ????

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

org.springframework.data.redis.cache.RedisCache#evict

對於 JDBC 事務而言,想要提交事務,那就必須要調用java.sql.Connection#commit方法。由於筆者此處使用的是 MySQL 數據庫,所以這裏對應的實現類爲com.mysql.jdbc.ConnectionImpl#commit。於是,同樣在該方法加一個斷點。

com.mysql.jdbc.ConnectionImpl#commit

打上斷點之後,讓我們來運行程序。

demo程序

在執行 save 方法之前,通過調用 getById 方法已經將對應的數據緩存到了 redis 中。同時,數據庫中 countNumber 的值爲 1。

添加緩存到redis中

程序再向下運行,可以發現,首先命中了org.springframework.data.redis.cache.RedisCache#evict方法的斷點,執行完該方法之後,可以看到,對應的緩存數據已被清除。

緩存已被清除

因爲還沒有中事務提交的斷點,所以此時很明顯數據庫中對應 id 爲 1 的記錄的 countNumber 值依舊爲 1。

數據庫中的記錄

程序再向下執行,則執行事務提交。

提交事務

執行完 commit 方法之後,事務提交,對應記錄更新成功。

更新成功

到這裏也就解決了本文開篇所提到的問題,我們希望程序是先提交事務,然後更新緩存。而真正的執行順序是,先清除緩存,然後提交事務

那這樣會有什麼問題呢?先清除緩存,然後在事務還沒有提交之前,程序就收到了用戶的請求,發現緩存中沒有數據,則去數據庫中獲取數據(事務還沒有提交則獲取到舊值),同時將獲取的數據添加到緩存中。此時會導致數據庫和緩存數據不一致。

如何解決 ????

方案 1:修改代碼,縮小事務範圍

事務是一個很容易出問題的操作,@Transactional事務不要濫用 ,用的時候要儘可能的縮小事務範圍,在事務方法中只做事務相關的操作。引用阿里巴巴 Java 開發手冊的一句話:

image.png
縮小事務範圍

方案 2:修改 AOP 執行順序

如果可以改成先提交事務,再清除緩存,一樣可以解決這個問題。那 Spring 中有沒有什麼方法可以去修改 AOP 的執行順序呢?

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

step into
CglibAopProxy.DynamicAdvisedInterceptor#intercept

可以看到,執行 save 方法之前,被CglibAopProxy.DynamicAdvisedInterceptor#intercept方法所攔截了。

在 SpringBoot2.0 之後,SpringBoot 中 AOP 的默認實現被設置成了默認使用 CGLIB 來實現了。具體可以閱讀筆者之前的文章:

https://mp.weixin.qq.com/s/oyH4GVwJeG24GVqLM48bVg

Spring5 AOP 默認使用 CGLIB ?從現象到源碼的深度分析

image.png

通過 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 內元素的順序呢?

通過查看BeanFactoryCacheOperationSourceAdvisorBeanFactoryTransactionAttributeSourceAdvisor的源碼可知,這兩個類均繼承了org.springframework.aop.support.AbstractPointcutAdvisor,而AbstractPointcutAdvisor這個抽象類實現了org.springframework.core.Ordered接口。

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

org.springframework.aop.support.AbstractPointcutAdvisor

BeanFactoryTransactionAttributeSourceAdvisor爲例,order 的值來自於AnnotationAttributes enableTx對象的某個屬性。

ProxyTransactionManagementConfiguration#transactionAdvisor

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

AbstractTransactionManagementConfiguration#setImportMetadata
@EnableTransactionManagement

同理,@EnableCaching註解上也可以配置 order,這裏不在贅述。

下面,我們就來嘗試解決這個問題,看能否通過配置 order 來修改 AOP 的執行順序。

修改AOP執行順序

通過@EnableCaching(order = Ordered.HIGHEST_PRECEDENCE)這個屬性值的配置,運行程序之後,的確做到了先提交事務,再清理緩存的效果,bug 修復成功~~

至於這個 order 設置是怎麼生效的,本文就不在此進行相關說明了。感興趣的讀者可以自行參閱相關源碼,對應的源碼在org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean,同時使用的比較器爲:org.springframework.core.annotation.AnnotationAwareOrderComparator

Advice Ordering

看到這裏不知道讀者有沒有疑問,優先級越高不是應該越先執行嗎?!緩存 AOP 的優先級最高怎麼比事務提交 AOP 執行的時機要晚呢?

我們來查閱一下 Spring 的官方文檔:

https://docs.spring.io/spring/docs/5.2.1.RELEASE/spring-framework-reference/core.html#aop-ataspectj-advice-ordering

Advice Ordering

簡單翻譯一下:(這個英文翻譯有點難,建議大家閱讀原文)

當多個 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 方法

AOP

總結

當@Transactional 遇到@CacheEvict,默認設置的情況下,可能會因爲先清除緩存後提交事務,從而產生緩存和數據庫數據不一致的問題。

同時,文本也提出了兩種解決方案。但是,筆者更建議使用方案 1(截圖有錯誤,原方法不需要添加事務註解),因爲方案 1 更多的是體現了一種編程思想,讓事務方法儘可能的小。

熱文推薦

得虧了它,我才把潛藏那麼深的Bug挖出來

驚訝!緩存剛Put再Get居然獲取不到?

好機會,我要幫女同事解決Maven衝突問題

上線前一個小時,dubbo這個問題可把我折騰慘了

爲了控制Bean的加載我使出了這些殺手鐗

如有收穫,點個在看,誠摯感謝

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章