你可能不知道這一點,在 .NET Framework 4.5.0 版本中包含有一個關於 System.Transactions.TransactionScope 在與 async/await 一起工作時會產生的一個嚴重的 bug 。由於這個錯誤,TransactionScope 不能在異步代碼中正常操作,它可能更改事務的線程上下文,導致在處理事務作用域時拋出異常。
這是一個很大的問題,因為它使得涉及事務的異步代碼極易出錯。
好消息是,在 .NET Framework 4.5.1 版本中,微軟發布了這個 "異步連接" 錯誤的修復程序。作為開發者的我們需要明確的做到以下兩點:
- 如果說你在 TransactionScope 代碼中使用 async/await,你需要將框架升級到 .NET 4.5.1 或以上版本。
- 在有包裝異步代碼的 TransactionScope 的構造函數中指定
TransactionScopeAsyncFlowOption.Enabled .
TransactionScope
System.Transactions.TransactionScope 類允許我們在事務中包裝數據庫代碼,基礎結構代碼,有時甚至包括第三方代碼(如果第三方庫支持)。 然后,代碼只在我們實際想提交(或完成)事務時才執行操作。
只要 TransactionScope 中的所有代碼都在同一線程上執行,調用堆棧上的所有代碼都可以與代碼中定義的 TransactionScope 一起參與。 我們可以在父事務作用域內嵌套作用域或創建新的獨立作用域。 我們甚至可以創建 TransactionScope 的副本,並將副本傳遞到另一個線程並連接回調用線程。 通過使用事務作用域包裝代碼,使用隱式事務模型,也稱為環境事務。
如下代碼:
public void TransactionScopeAffectsCurrentTransaction() { Debug.Assert(Transaction.Current == null); using (var tx = new TransactionScope()) { Debug.Assert(Transaction.Current != null); SomeMethodInTheCallStack(); tx.Complete(); } Debug.Assert(Transaction.Current == null); } private static void SomeMethodInTheCallStack() { Debug.Assert(Transaction.Current != null); }
正如我們可以看到使用塊外面的 Transaction.Current 屬性為 null。 在使用塊內 Transaction.Current 屬性不為 null。 即使在調用堆棧中的方法像 SomeMethodInTheCallStack 可以訪問 Transaction.Current,前提是它要被包裹在使用塊中。
TransactionScope 的好處是,如果需要,本地事務自動升級到分布式事務。 事務范圍還簡化了對事務的編程,如果你喜歡隱式的顯式。
TransactionFlowInterruptedException
當 async / await 引入了 C#5.0 和 .NET 4.5,一個小小的細節被完全忘記。 當在一個包裝 TransactionScope 下調用一個異步方法時,編譯器引入的底層狀態機沒有正確地“浮動”事務(原文 "float")。 讓我們將我們關於 TransactionScope 如何在同步代碼中工作的知識應用到異步代碼。
有如下代碼:
public async Task TransactionScopeWhichDoesntBehaveLikeYouThinkItShould() { using (var tx = new TransactionScope()) { await SomeMethodInTheCallStackAsync() .ConfigureAwait(false); tx.Complete(); } } private static async Task SomeMethodInTheCallStackAsync() { await Task.Delay(500).ConfigureAwait(false); }
不幸的是,它不工作的方式。 代碼幾乎(但只是幾乎)執行類似於同步版本,但如果項目這個代碼是寫在目標 .NET Framework 4.5,當我們到達使用塊的結束,並嘗試處置 TransactionScope 時拋出以下異常 :
System.InvalidOperationException:一個 TransactionScope 必須處理在它被創建的同一個線程。
為了使TransactionScope和async正常工作,我們需要將我們的項目升級到.NET 4.5.1。
TransactionScopeAsyncFlowOption
在 .NET 4.5.1中,TransactionScope 有一個名為 TransactionScopeAsyncFlowOption 的新枚舉,可以在構造函數中提供。 您必須通過指定,明確地選擇跨線程連續的事務流,如下:
using (var tx = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) { await SomeMethodInTheCallStackAsync() .ConfigureAwait(false); tx.Complete(); }
你可能很好奇,默認的 TransactionScopeAsyncFlowOption 是 Suppress(阻止的),因為微軟想避免破壞 .NET 4.5.0 版本中代碼庫中行為。
最后
使用 TransactionScope 結合 async / await 時,你應該更新所有使用 TransactionScope 的代碼路徑以啟用 TransactionScopeAsyncFlowOption.Enabled 。 這樣才能使事務能夠正確地流入異步代碼,防止在TransactionScope下使用時業務邏輯不正常。