業(yè)務(wù)上線后出現(xiàn)了一個bug: 用戶配置/刪除課程時瞭亮,根據(jù)已配置課程數(shù)據(jù)生成欄目樹時好時壞家乘,具體表現(xiàn)為:某種時刻刷新的欄目樹是數(shù)據(jù)未入庫之前的舊數(shù)據(jù)蝗羊。但是全量刷新類目接口穩(wěn)定不報錯,兩個公用一個接口仁锯。
查詢具體代碼發(fā)現(xiàn)耀找,刷新類目接口是在課程配置@Transactional中調(diào)用了@async調(diào)用的異步方法,分析业崖,很有可能是這兒導(dǎo)致野芒,數(shù)據(jù)未commit完畢,異步刷新類目方法已經(jīng)調(diào)用執(zhí)行完畢了双炕。
其中stackoverflow上同樣有人提出了相同的問題:
https://stackoverflow.com/questions/51833306/using-async-inside-a-transaction-in-spring-application
具體的解決辦法狞悲,考慮有兩種:
- 使用event事件機(jī)制,當(dāng)數(shù)據(jù)全部更新完畢后通知異步方法執(zhí)行妇斤,這是基于觀察者模式
- 使用TransactionSynchronizationManager摇锋,重寫其中的 afterCommit 方法丹拯,標(biāo)明在數(shù)據(jù)commit完畢后執(zhí)行。
具體偽代碼實(shí)現(xiàn)如下:
public class CourseServiceImpl implements CourseService {
/**
* 配置課程
*/
public void addRealmCourseList(){
//更新數(shù)據(jù)庫操作
update();
//調(diào)用異步方式
executeAfterTransactionCommits(()->{
//具體異步方法
...
});
}
/**
* commit之后執(zhí)行
*/
private void executeAfterTransactionCommits(Runnable task) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
task.run();
}
});
}
}
值得注意的是乱投,除去這種異常case之外咽笼,@Transactional
與@Async
聯(lián)合使用還會導(dǎo)致設(shè)置的Transaction注解不生效。原因其實(shí)也很簡單:
- Spring 實(shí)現(xiàn)這兩個注解的方式都是通過
AOP
戚炫。 - 在實(shí)現(xiàn)時剑刑,Async注解強(qiáng)制覆蓋
AOP
的order為最小值(它認(rèn)為Async應(yīng)該是執(zhí)行的AOP
鏈中的第一個advisor) - 但是在實(shí)現(xiàn)Transactional注解時,卻沒有覆蓋order双肤,這意味著它仍然為默認(rèn)的Integer.MAX_VALUE施掏,order可配置。所以異步切面會先于事務(wù)切面執(zhí)行茅糜。
- 假設(shè)
@Transactional
能先于Async切面執(zhí)行七芭,但由于spring事務(wù)管理依賴的是ThreadLocal,所以在開啟的異步線程里面感知不到事務(wù)蔑赘,說細(xì)點(diǎn)就是在Spring開啟事務(wù)之后狸驳,會設(shè)置一個連接到當(dāng)前線程,但這個時候又開啟了一個新線程缩赛,執(zhí)行實(shí)際的SQL代碼時耙箍,通過ThreadLocal獲取不到連接就會開啟新連接,也不會設(shè)置autoCommit酥馍,所以這個函數(shù)整體將沒有事務(wù)辩昆。
其中,在spring源碼中同樣有人提到了這樣一個issue旨袒,目前已經(jīng)關(guān)閉:
https://github.com/spring-projects/spring-framework/issues/11806
由此可見汁针,當(dāng)我們業(yè)務(wù)中有需要Transaction與async同時使用時,一定要小心使用砚尽。