译文:TransactionScope 与 Async/Await
你可能不知道这一点,在 .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下使用时业务逻辑不正常。