Spring中@Transactional與@Async共同使用

banq發表於2024-04-14

在本文中,我們將研究Spring 框架的@Transactional和@Async註解之間的相容性。

什麼是@Transactional和@Async
@Transactional註釋從許多其他註釋建立原子程式碼塊。所以,如果一個區塊異常完成,所有部分都會回滾。因此,新建立的原子單元只有在其所有部分都成功時才能透過提交成功完成。

建立事務使我們能夠避免程式碼中的部分失敗,從而提高資料一致性。

另一方面,@Async告訴Spring被註解的單元可以與呼叫執行緒並行執行。換句話說,如果我們從執行緒呼叫@Async方法或類,Spring 將在具有不同上下文的另一個執行緒中執行其程式碼。

定義非同步程式碼可以透過與呼叫執行緒並行執行單元來提高執行時間效能。

在某些情況下,我們需要程式碼的效能和一致性。使用 Spring,我們可以混合@Transactional和@Async來實現這兩個目標,只要我們注意如何一起使用註釋即可。

在以下部分中,我們將探討不同的場景。

@Transactional和@Async可以一起工作嗎?
如果我們沒有正確實現非同步和事務性程式碼,則可能會帶來資料不一致等問題。

關注Spring的事務上下文和上下文之間的資料傳播是充分利用@Async和@Transactional並避免陷阱的基礎。

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()方法儲存新更新的帳戶。

在這個簡單的示例中,存在一些潛在的故障。例如,我們可能找不到喜愛的帳戶並因異常而失敗。或者,depositorAccount 的save()操作完成,但favoredAccount的 save() 操作失敗。這些被定義為部分失敗,因為失敗之前發生的事情無法撤消。

因此,如果我們沒有透過事務正確管理程式碼,部分故障就會產生資料一致性問題。例如,我們可能會從一個帳戶中取出資金,但沒有有效地將其轉移到另一個帳戶。

2.從@Async呼叫@Transactional
如果我們從@Async方法呼叫@Transactional方法,Spring會正確管理事務並傳播其上下文,確保資料一致性。

例如,讓我們從 @Async呼叫者呼叫@Transactional  Transfer()方法:

@Async
public void transferAsync(Long depositorId, Long favoredId, BigDecimal amount) {
    transfer(depositorId, favoredId, amount);
    <font>// other async operations, isolated from transfer<i>
}
@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.從 @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() { <font>// logic to print the receipt with the results of the transfer }<i>

Transfer ()邏輯與之前相同,但現在我們呼叫printReceipt() 來列印轉賬結果。由於printReceipt()是@Async,Spring 在具有另一個上下文的不同執行緒上執行其程式碼。

問題是收據資訊取決於正確執行整個transfer()方法。此外,  printReceipt()和儲存到資料庫中的其餘Transfer()程式碼在具有不同資料的不同執行緒上執行,使得應用程式行為不可預測。例如,我們可能會列印未成功儲存到資料庫中的匯款交易的結果。

因此,為了避免這種資料一致性問題,我們必須避免從@Transactional呼叫@Async方法,因為不會發生執行緒上下文傳播。

4.在類級別使用@Transactional
使用@Transactional定義一個類 ,使其所有公共方法都可用於 Spring 事務管理。因此,註釋會同時為所有方法建立事務。

在類級別 使用@Transactional時可能發生的一件事是在同一方法中將其與@Async混合。實際上,我們圍繞該方法建立一個事務單元,該單元在與呼叫執行緒不同的執行緒中執行:

@Transactional
public class AccountService {
    @Async
    public void transferAsync() {
        <font>// this is an async and transactional method<i>
    }
    public void transfer() {
       
// transactional method<i>
    }
}

在示例中,transferAsync()方法是事務性且非同步的。因此,它定義了一個事務單元並在不同的執行緒上執行。因此,它可用於事務管理,但不能與呼叫執行緒處於同一上下文中。

因此,如果出現故障, transferAsync()內部的程式碼就會回滾,因為它是@Transactional。但是,由於該方法也是 @Async, Spring 不會將呼叫上下文傳播給它。因此,在失敗場景中,Spring 不會回滾 trasnferAsync()之外的任何程式碼,就像我們呼叫一系列僅事務性方法時一樣。因此,這會遇到與從 @Transactional呼叫@Async相同的資料完整性問題。

類級註釋可以方便地編寫更少的程式碼來建立定義一系列完全事務性方法的類。

但是,在對程式碼進行故障排除時,這種混合的事務和非同步行為可能會造成混亂。例如,我們期望在發生故障時回滾僅事務性方法呼叫序列中的所有程式碼。但是,如果該序列的方法也是@Async,則該行為是意外的。

結論
在本教程中,我們從資料完整性的角度瞭解了何時可以安全地一起使用@Transactional和@Async註釋。

  • 一般來說,從@Async方法呼叫 @Transactional可以保證資料完整性,因為 Spring 正確地傳播相同的上下文。
  • 另一方面,當從@Transactional呼叫@Async時,我們可能會陷入資料完整性問題。

相關文章