轉自:Coder小黑
作者:coder小黑
如上圖所示,當@Transactional 遇到@CacheEvict,緩存放在 redis 中,這樣寫代碼會有什麼問題呢?你們的程序中是否寫着這樣的代碼呢?如果是,請你立刻修改!
思考 ????
首先,@Transactional
是給當前方法添加事務支持,是通過 AOP 動態代理實現的,在方法執行完之後才提交事務。其次,@CacheEvict
是在該方法執行完之後,清除 redis 中的緩存,也是使用 AOP 動態代理實現的。
那麼,上述方法想表達語義應該是:先保存對象,提交事務,然後清除緩存。但是,這樣寫真的能達到這個語義嗎?
Debug 尋找真相 ????
首先,執行清除緩存的是org.springframework.cache.Cache#evict
方法,此處又是使用 redis 作爲緩存的提供者,所以在清除緩存時,必然會調用 redis 緩存實現類的方法,即:org.springframework.data.redis.cache.RedisCache#evict
。於是,在該方法處加一個斷點。
對於 JDBC 事務而言,想要提交事務,那就必須要調用java.sql.Connection#commit
方法。由於筆者此處使用的是 MySQL 數據庫,所以這裏對應的實現類爲com.mysql.jdbc.ConnectionImpl#commit
。於是,同樣在該方法加一個斷點。
打上斷點之後,讓我們來運行程序。
在執行 save 方法之前,通過調用 getById 方法已經將對應的數據緩存到了 redis 中。同時,數據庫中 countNumber 的值爲 1。
程序再向下運行,可以發現,首先命中了org.springframework.data.redis.cache.RedisCache#evict
方法的斷點,執行完該方法之後,可以看到,對應的緩存數據已被清除。
因爲還沒有中事務提交的斷點,所以此時很明顯數據庫中對應 id 爲 1 的記錄的 countNumber 值依舊爲 1。
程序再向下執行,則執行事務提交。
執行完 commit 方法之後,事務提交,對應記錄更新成功。
到這裏也就解決了本文開篇所提到的問題,我們希望程序是先提交事務,然後更新緩存。而真正的執行順序是,先清除緩存,然後提交事務。
那這樣會有什麼問題呢?先清除緩存,然後在事務還沒有提交之前,程序就收到了用戶的請求,發現緩存中沒有數據,則去數據庫中獲取數據(事務還沒有提交則獲取到舊值),同時將獲取的數據添加到緩存中。此時會導致數據庫和緩存數據不一致。
如何解決 ????
方案 1:修改代碼,縮小事務範圍
事務是一個很容易出問題的操作,@Transactional
事務不要濫用 ,用的時候要儘可能的縮小事務範圍,在事務方法中只做事務相關的操作。引用阿里巴巴 Java 開發手冊的一句話:
方案 2:修改 AOP 執行順序
如果可以改成先提交事務,再清除緩存,一樣可以解決這個問題。那 Spring 中有沒有什麼方法可以去修改 AOP 的執行順序呢?
@Transactional
和@CacheEvict
都是通過動態代理來實現的,在執行 save 方法處打一個斷點,命中斷點之後,點擊Step Into
,就可以進入到代理對象的執行方法內。
可以看到,執行 save 方法之前,被CglibAopProxy.DynamicAdvisedInterceptor#intercept
方法所攔截了。
在 SpringBoot2.0 之後,SpringBoot 中 AOP 的默認實現被設置成了默認使用 CGLIB 來實現了。具體可以閱讀筆者之前的文章:
https://mp.weixin.qq.com/s/oyH4GVwJeG24GVqLM48bVg
Spring5 AOP 默認使用 CGLIB ?從現象到源碼的深度分析
通過 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 中的排序呢?
以BeanFactoryTransactionAttributeSourceAdvisor
爲例,order 的值來自於AnnotationAttributes enableTx
對象的某個屬性。
通過源碼可以發現,AnnotationAttributes enableTx
的屬性全部都來自於@EnableTransactionManagement
註解。
同理,@EnableCaching
註解上也可以配置 order,這裏不在贅述。
下面,我們就來嘗試解決這個問題,看能否通過配置 order 來修改 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 運行在同一個 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 方法。
總結
當@Transactional 遇到@CacheEvict,默認設置的情況下,可能會因爲先清除緩存後提交事務,從而產生緩存和數據庫數據不一致的問題。
同時,文本也提出了兩種解決方案。但是,筆者更建議使用方案 1(截圖有錯誤,原方法不需要添加事務註解),因爲方案 1 更多的是體現了一種編程思想,讓事務方法儘可能的小。
熱文推薦
如有收穫,點個在看,誠摯感謝