簡介
說明
本文介紹@Transactional與@CacheEvict聯合使用導緻的緩存與資料庫的一緻性問題的原因及解決方案。
注解的作用
@Transactional:給目前方法添加事務支援,是通過 AOP 動态代理實作的,在方法執行完之後送出事務。
@CacheEvict:在該方法執行完之後,清除 redis 中的緩存,也是使用 AOP 動态代理實作的。
問題引出
如果是@Transactional與@CacheEvict用在同一個方法上邊,是否有問題?
是否可以達到如下效果:先儲存到資料庫,送出資料庫事務,然後清除緩存?
打斷點分析
給@CacheEvict打斷點
執行清除緩存的是org.springframework.cache.Cache#evict方法,此處是使用 redis 作為緩存的提供者,是以在清除緩存時必然會調用 redis 緩存實作類的方法,即:org.springframework.data.redis.cache.RedisCache#evict。于是,在該方法處加一個斷點:
給@Transactional打斷點
對于 JDBC 事務而言,想要送出事務就要調用java.sql.Connection#commit方法。由于筆者此處使用的是 MySQL 資料庫,是以這裡對應的實作類為com.mysql.jdbc.ConnectionImpl#commit。于是,同樣在該方法加一個斷點:
測試程式
在執行 save 方法之前,通過調用 getById 方法已經将對應的資料緩存到了 redis 中。此時,Redis和資料庫中 countNumber 的值為 1。
測試
調用save方法,id為1,countNumber為2。
- 首先命中了org.springframework.data.redis.cache.RedisCache#evict方法的斷點,執行完該方法之後,對應的Redis資料已被清除(countNumber為空)。此時資料庫裡countNumber仍然是1。
- 再向下運作,到達com.mysql.jdbc.ConnectionImpl#commit,執行事務送出,執行完後,資料庫裡countNumber變成2
我們希望先送出事務,然後更新緩存。而真正的執行順序是,先清除緩存,然後送出事務。
這樣存在的問題:資料庫和緩存資料不一緻
先清除緩存,然後在事務還沒有送出之前,程式就收到了使用者的請求,發現緩存中沒有資料,則去資料庫中擷取資料(事務還沒有送出則擷取到舊值),同時将擷取的資料添加到緩存中。此時會導緻資料庫和緩存資料不一緻。
解決方案
方案1:将事務單獨拿出為一個方法
方案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 方法。
修改AOP順序的方法分析
斷點入口
@Transactional和@CacheEvict都是通過動态代理來實作的,在執行 save 方法處打一個斷點,命中斷點之後,點選Step Into,就可以進入到代理對象的執行方法内。
攔截的方法
可以看到,執行 save 方法之前,被CglibAopProxy.DynamicAdvisedInterceptor#intercept方法所攔截了。(在 SpringBoot2.0 之後,SpringBoot 中 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 中的排序呢?
Order的設定方法
以BeanFactoryTransactionAttributeSourceAdvisor為例,order 的值來自于AnnotationAttributes enableTx對象的某個屬性。
通過源碼可以發現,AnnotationAttributes enableTx的屬性全部都來自于@EnableTransactionManagement注解。