@Transactional 和 @Async 可以一起工作嗎?
一、簡介
在本文中,我們將研究 Spring 框架的@Transactional
和@Async
註解之間的兼容性。
2.理解@Transactional
和@Async
@Transactional
註釋從許多其他註釋創建原子代碼塊。所以,如果一個區塊異常完成,所有部分都會回滾。因此,新創建的原子單元只有在其所有部分都成功時才能透過提交成功完成。
創建事務使我們能夠避免程式碼中的部分失敗,從而提高資料一致性。
另一方面, @Async
告訴Spring被註解的單元可以與呼叫執行緒並行運行。換句話說,如果我們從執行緒呼叫@Async
方法或類,Spring 將在具有不同上下文的另一個執行緒中執行其程式碼。
定義非同步程式碼可以透過與呼叫執行緒並行執行單元來提高執行時間效能。
在某些情況下,我們需要程式碼的效能和一致性。使用 Spring,我們可以混合@Transactional
和@Async
來實現這兩個目標,只要我們注意如何一起使用註解即可。
在以下部分中,我們將探討不同的場景。
3. @Transactional
和@Async
可以一起工作嗎?
如果我們沒有正確實現非同步和事務性程式碼,則可能會帶來資料不一致等問題。
關注Spring的事務上下文和上下文之間的資料傳播是充分利用@Async
和@Transactional
並避免陷阱的基礎。
3.1.建立演示應用程式
我們將使用銀行服務的轉帳功能來說明交易和非同步代碼的使用。
簡而言之,我們可以透過從一個帳戶中取出資金並將其添加到另一個帳戶來實現轉帳。我們可以將其想像為資料庫操作,例如選擇相關帳戶並更新其資金餘額:
public void transfer(Long depositorId, Long favoredId, BigDecimal amount) {
Account depositorAccount = accountRepository.findById(depositorId)
.orElseThrow(IllegalArgumentException::new);
Account favoredAccount = accountRepository.findById(favoredId)
.orElseThrow(IllegalArgumentException::new);
depositorAccount.setBalance(depositorAccount.getBalance().subtract(amount));
favoredAccount.setBalance(favoredAccount.getBalance().add(amount));
accountRepository.save(depositorAccount);
accountRepository.save(favoredAccount);
}
我們首先使用findById()
來尋找涉及的帳戶,如果給定的 ID 找不到該帳戶,則拋出IllegalArgumentException
。
然後,我們用新金額更新檢索到的帳戶。最後,我們使用CrudRepository
的save()
方法儲存新更新的帳戶。
在這個簡單的範例中,存在一些潛在的故障。例如,我們可能找不到favoredAccount
並因異常而失敗。或者, depositorAccount
的save()
操作完成,但favoredAccount
的 save() 作業失敗。這些被定義為部分失敗,因為失敗之前發生的事情無法撤銷。
因此,如果我們沒有透過交易正確管理程式碼,部分故障就會產生資料一致性問題。例如,我們可能會從一個帳戶中取出資金,但沒有有效地將其轉移到另一個帳戶。
3.2.從@Async
呼叫@Transactional
如果我們從@Async
方法呼叫@Transactional
方法,Spring會正確管理交易並傳播其上下文,確保資料一致性。
例如,讓我們從Async
呼叫者呼叫@Transactional
transfer()
方法:
@Async
public void transferAsync(Long depositorId, Long favoredId, BigDecimal amount) {
transfer(depositorId, favoredId, amount);
// other async operations, isolated from transfer
}
@Transactional
public void transfer(Long depositorId, Long favoredId, BigDecimal amount) {
Account depositorAccount = accountRepository.findById(depositorId)
.orElseThrow(IllegalArgumentException::new);
Account favoredAccount = accountRepository.findById(favoredId)
.orElseThrow(IllegalArgumentException::new);
depositorAccount.setBalance(depositorAccount.getBalance().subtract(amount));
favoredAccount.setBalance(favoredAccount.getBalance().add(amount));
accountRepository.save(depositorAccount);
accountRepository.save(favoredAccount);
}
transferAsync()
方法與不同上下文中的呼叫執行緒並行運行,因為它是@Async
。
然後,我們呼叫事務性的transfer()
方法來運行關鍵的業務邏輯。在這種情況下,Spring 正確地將transferAsync()
線程上下文傳播到transfer()
。因此,我們不會在該交互中丟失任何資料。
transfer()
方法定義了一組關鍵的資料庫操作,如果發生故障則必須回滾這些操作。 Spring只處理transfer()
事務,它將transfer()
主體以外的所有程式碼與事務隔離。因此,Spring 僅在發生故障時回滾transfer()
程式碼。
從@Async
方法呼叫@Transactional
可以透過與呼叫執行緒並行執行操作來提高效能,而不會在特定內部操作中出現資料不一致。
3.3.從@Transactional
呼叫@Async
Spring目前使用ThreadLocal
來管理目前執行緒事務。因此,它不會在應用程式的不同執行緒之間共享執行緒上下文。
因此,如果@Transactional
方法呼叫@Async
方法,Spring 不會傳播事務的相同執行緒上下文。
為了說明這一點,我們在transfer()
中加入對非同步printReceipt()
方法的呼叫:
@Async
public void transferAsync(Long depositorId, Long favoredId, BigDecimal amount) {
transfer(depositorId, favoredId, amount);
}
@Transactional
public void transfer(Long depositorId, Long favoredId, BigDecimal amount) {
Account depositorAccount = accountRepository.findById(depositorId)
.orElseThrow(IllegalArgumentException::new);
Account favoredAccount = accountRepository.findById(favoredId)
.orElseThrow(IllegalArgumentException::new);
depositorAccount.setBalance(depositorAccount.getBalance().subtract(amount));
favoredAccount.setBalance(favoredAccount.getBalance().add(amount));
printReceipt();
accountRepository.save(depositorAccount);
accountRepository.save(favoredAccount);
}
@Async public void printReceipt() { // logic to print the receipt with the results of the transfer }
transfer()
邏輯與之前相同,但現在我們呼叫printReceipt()
來列印轉帳結果。由於printReceipt()
是@Async
,Spring 在具有另一個上下文的不同執行緒上執行其程式碼。
問題是收據資訊取決於正確執行整個transfer()
方法。此外, printReceipt()
和保存到資料庫中的其餘transfer()
程式碼在具有不同資料的不同執行緒上運行,從而使應用程式行為變得不可預測。例如,我們可能會列印未成功儲存到資料庫中的匯款交易的結果。
因此,為了避免這種資料一致性問題,我們必須避免從@Transactional
呼叫@Async
方法,因為不會發生執行緒上下文傳播。
3.4.在類別層級使用@Transactional
使用@Transactional
定義一個類,使其所有公共方法都可用於 Spring 事務管理。因此,註解會同時為所有方法建立事務。
在類別層級使用@Transactional
時可能發生的一件事是在相同方法中將其與@Async
混合。實際上,我們圍繞該方法創建一個事務單元,該單元在與調用線程不同的線程中運行:
@Transactional
public class AccountService {
@Async
public void transferAsync() {
// this is an async and transactional method
}
public void transfer() {
// transactional method
}
}
在範例中, transferAsync()
方法是事務性且非同步的。因此,它定義了一個事務單元並在不同的執行緒上運行。因此,它可用於事務管理,但不能與呼叫執行緒處於同一上下文中。
因此,如果發生故障, transferAsync()
內部的程式碼就會回滾,因為它是@Transactional
。但是,由於該方法也是@Async,
Spring 不會將呼叫上下文傳播給它。因此,在失敗場景中,Spring 不會回滾trasnferAsync()
以外的任何程式碼,就像我們呼叫一系列僅事務性方法一樣。因此,這會遇到與從@Transactional
呼叫@Async
相同的資料完整性問題。
類別級註解可以方便地編寫更少的程式碼來建立定義一系列完全事務性方法的類別。
但是,在對程式碼進行故障排除時,這種混合的事務和異步行為可能會造成混亂。例如,我們期望在發生故障時回滾僅事務性方法呼叫序列中的所有程式碼。但是,如果該序列的方法也是@Async
,則該行為是意外的。
4。結論
在本教程中,我們從資料完整性的角度了解了何時可以安全地一起使用@Transactional
和@Async
註解。
一般來說,從@Async
方法呼叫@Transactional
可以保證資料完整性,因為 Spring 正確地傳播相同的上下文。
另一方面,當從@Transactional
呼叫@Async
時,我們可能會陷入資料完整性問題。
與往常一樣,原始碼可以在 GitHub 上取得。