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