@Transactional + @Async 有大坑!
@Transactional 和 @Async 這兩個(gè)注解更是開(kāi)發(fā)者們常常使用的得力工具。然而,當(dāng)這兩個(gè)注解相遇,它們能否和諧共處,發(fā)揮出最大的效能呢?
相信很多開(kāi)發(fā)者都沒(méi)有深入思考過(guò)這個(gè)問(wèn)題。今天,就讓我們一起深入探討一下 Spring 框架中 @Transactional 和 @Async 注解之間的兼容性。
深入理解 @Transactional 和 @Async
@Transactional 注解就像是一位嚴(yán)謹(jǐn)?shù)墓芗?,它?huì)創(chuàng)建一個(gè)原子代碼塊。在這個(gè)代碼塊里,所有的操作都被視為一個(gè)整體。一旦其中某個(gè)操作出現(xiàn)異常,就如同多米諾骨牌一樣,所有已經(jīng)執(zhí)行的部分都會(huì)被回滾。只有當(dāng)這個(gè)原子單元中的所有操作都成功完成時(shí),才會(huì)通過(guò)提交操作正式生效。使用事務(wù)機(jī)制,我們可以有效地避免代碼出現(xiàn)部分失敗的情況,從而大大提高數(shù)據(jù)的一致性。
@Async 注解則像是一位充滿活力的短跑選手,它告訴 Spring,被注解的方法或類可以與調(diào)用線程并行運(yùn)行。當(dāng)我們從一個(gè)線程調(diào)用一個(gè) @Async 方法時(shí),Spring 會(huì)在另一個(gè)具有不同上下文的線程中啟動(dòng)該方法的執(zhí)行。這種異步執(zhí)行的方式可以顯著提高程序的執(zhí)行效率,尤其是在處理一些耗時(shí)的操作時(shí)。
在某些復(fù)雜的業(yè)務(wù)場(chǎng)景中,我們既希望代碼能夠保證數(shù)據(jù)的一致性,又希望能夠提高執(zhí)行性能。在 Spring 中,我們確實(shí)可以嘗試將 @Transactional 和 @Async 結(jié)合使用,以實(shí)現(xiàn)這兩個(gè)看似矛盾的目標(biāo)。但在實(shí)際操作中,我們必須格外小心,注意如何正確地使用這兩個(gè)注解。
@Transactional 和 @Async 能一起使用嗎?
1. 構(gòu)建示例應(yīng)用:銀行轉(zhuǎn)賬功能
為了更好地說(shuō)明事務(wù)和異步代碼的使用,我們以銀行的轉(zhuǎn)賬功能為例。簡(jiǎn)單來(lái)說(shuō),轉(zhuǎn)賬就是從一個(gè)賬戶中取出一定金額的錢,然后將其添加到另一個(gè)賬戶中。這一系列操作可以看作是對(duì)數(shù)據(jù)庫(kù)中賬戶信息的更新操作。
圖片
我們的具體實(shí)現(xiàn)步驟如下:首先,使用 findById() 方法根據(jù)賬戶 ID 查找對(duì)應(yīng)的賬戶信息。如果給定的 ID 沒(méi)有找到對(duì)應(yīng)的賬戶,就會(huì)拋出 IllegalArgumentException 異常。
接著,我們會(huì)用新的金額更新檢索到的賬戶信息。最后,使用 CrudRepository 的 save() 方法將更新后的賬戶信息保存到數(shù)據(jù)庫(kù)中。
在這個(gè)看似簡(jiǎn)單的例子中,其實(shí)存在著一些潛在的風(fēng)險(xiǎn)點(diǎn)。比如,我們可能找不到目標(biāo)賬戶,從而導(dǎo)致操作因異常而失敗。又或者,save() 操作在更新轉(zhuǎn)出賬戶時(shí)成功了,但在更新轉(zhuǎn)入賬戶時(shí)卻失敗了。
這些情況都屬于部分失敗,因?yàn)樵谑≈耙呀?jīng)執(zhí)行的操作無(wú)法撤銷。如果我們不使用事務(wù)機(jī)制來(lái)管理這些代碼,部分失敗就很可能會(huì)導(dǎo)致數(shù)據(jù)一致性問(wèn)題。
例如,我們可能從一個(gè)賬戶中移除了資金,但卻沒(méi)有成功將其轉(zhuǎn)移到另一個(gè)賬戶中。
2. 從 @Async 調(diào)用 @Transactional
當(dāng)我們從 @Async 方法中調(diào)用 @Transactional 方法時(shí),Spring 會(huì)發(fā)揮其強(qiáng)大的管理能力,正確地管理事務(wù)并傳播其上下文,從而確保數(shù)據(jù)的一致性。
讓我們來(lái)看一個(gè)具體的例子。假設(shè)我們有一個(gè) transferAsync() 方法,它被 @Async 注解修飾,這意味著它會(huì)在一個(gè)與調(diào)用線程不同的上下文中并行運(yùn)行。在這個(gè)方法中,我們調(diào)用了一個(gè)被 @Transactional 注解修飾的 transfer() 方法來(lái)執(zhí)行關(guān)鍵的業(yè)務(wù)邏輯。
圖片
在這種情況下,Spring 會(huì)將 transferAsync() 線程的上下文正確地傳播給 transfer() 方法。這樣一來(lái),我們就不會(huì)在這個(gè)交互過(guò)程中丟失任何數(shù)據(jù)。
transfer() 方法定義了一組關(guān)鍵的數(shù)據(jù)庫(kù)操作,如果在執(zhí)行過(guò)程中發(fā)生任何失敗,Spring 會(huì)自動(dòng)回滾這些操作。需要注意的是,Spring 只處理 transfer() 方法內(nèi)部的事務(wù),會(huì)將 transfer() 方法體外的所有代碼與事務(wù)隔離開(kāi)來(lái)。因此,只有當(dāng) transfer() 方法內(nèi)部發(fā)生失敗時(shí),Spring 才會(huì)回滾其代碼。
從 @Async 方法中調(diào)用 @Transactional 方法是一種非常巧妙的設(shè)計(jì),它既可以通過(guò)并行執(zhí)行操作來(lái)提高性能,又可以確保特定內(nèi)部操作的數(shù)據(jù)一致性,實(shí)現(xiàn)了性能和數(shù)據(jù)一致性的雙贏。
3. 從 @Transactional 調(diào)用 @Async
Spring 目前使用 ThreadLocal 來(lái)管理當(dāng)前線程的事務(wù),這意味著它不會(huì)在應(yīng)用程序的不同線程之間共享線程上下文。因此,如果 @Transactional 方法調(diào)用 @Async 方法,Spring 不會(huì)將同一事務(wù)的線程上下文傳播給 @Async 方法。
為了更直觀地理解這個(gè)問(wèn)題,我們?cè)?nbsp;transfer() 方法內(nèi)部添加一個(gè)對(duì)異步 printReceipt() 方法的調(diào)用。
圖片
transfer() 方法的邏輯與之前相同,只是增加了調(diào)用 printReceipt() 方法來(lái)打印轉(zhuǎn)賬結(jié)果的操作。由于 printReceipt() 方法被 @Async 注解修飾,Spring 會(huì)在另一個(gè)上下文的不同線程上運(yùn)行其代碼。
這里就存在一個(gè)問(wèn)題,收據(jù)信息的打印依賴于 transfer() 方法的整個(gè)正確執(zhí)行。然而,printReceipt() 方法和 transfer() 方法中保存到數(shù)據(jù)庫(kù)的其余代碼在不同的線程上運(yùn)行,且數(shù)據(jù)不同,這就使得應(yīng)用程序的行為變得不可預(yù)測(cè)。例如,我們可能會(huì)打印一個(gè)在成功保存到數(shù)據(jù)庫(kù)之前的轉(zhuǎn)賬交易結(jié)果。
為了避免這種數(shù)據(jù)一致性問(wèn)題,我們必須避免從 @Transactional 方法中調(diào)用 @Async 方法,因?yàn)樵谶@種情況下不會(huì)發(fā)生線程上下文的傳播。
4. 在類級(jí)別使用 @Transactional
使用 @Transactional 注解定義一個(gè)類時(shí),該類的所有公共方法都會(huì)被納入 Spring 的事務(wù)管理范圍。這意味著該注解會(huì)一次性為所有方法創(chuàng)建事務(wù)。
在類級(jí)別使用 @Transactional 時(shí),可能會(huì)出現(xiàn)同一個(gè)方法同時(shí)使用 @Async 注解的情況。實(shí)際上,我們是在該方法的周圍創(chuàng)建了一個(gè)事務(wù)單元,并且這個(gè)事務(wù)單元會(huì)在與調(diào)用線程不同的線程上運(yùn)行。
圖片
在上面的例子中,transferAsync() 方法既是事務(wù)性的又是異步的。因此,它定義了一個(gè)事務(wù)單元并在不同的線程上運(yùn)行。因此,它可用于事務(wù)管理,但不在與調(diào)用線程相同的上下文中。
因此,如果發(fā)生失敗,transferAsync() 內(nèi)部的代碼會(huì)回滾,因?yàn)樗?nbsp;@Transactional 的。然而,由于該方法也是 @Async 的,Spring 不會(huì)將調(diào)用上下文傳播給它。因此,在失敗的情況下,Spring 不會(huì)回滾 trasnferAsync() 之外的任何代碼,就像我們調(diào)用一系列僅包含事務(wù)的方法時(shí)一樣。因此,這與從 @Transactional 中調(diào)用 @Async 面臨相同的數(shù)據(jù)完整性問(wèn)題。
類級(jí)別的注解對(duì)于編寫(xiě)較少代碼以創(chuàng)建定義一系列完全事務(wù)性方法的類非常有用。
但是,這種混合的事務(wù)性和異步行為在調(diào)試代碼時(shí)可能會(huì)造成混淆。例如,我們期望在發(fā)生失敗時(shí),一系列僅包含事務(wù)的方法調(diào)用中的所有代碼都會(huì)回滾。然而,如果這一系列方法中的某個(gè)方法也是 @Async 的,那么行為就會(huì)出乎意料。
總結(jié)
在本教程中,我們從數(shù)據(jù)完整性的角度學(xué)習(xí)了何時(shí)可以安全地將 @Transactional 和 @Async 注解一起使用。
通常,從 @Async 方法中調(diào)用 @Transactional 方法可以保證數(shù)據(jù)完整性,因?yàn)?Spring 會(huì)正確地傳播相同的上下文。
但是,從 @Transactional 中調(diào)用 @Async 方法時(shí),我們可能會(huì)遇到數(shù)據(jù)完整性問(wèn)題。