65.9K
CodeProject 正在变化。 阅读更多。
Home

关于 TransactionScope 的一切

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (178投票s)

2013 年 12 月 1 日

CPOL

18分钟阅读

viewsIcon

467768

downloadIcon

2893

在本文中,我将通过事务相关的理论和代码示例,解释在各种场景下如何使用 TransactionScope 及其各种选项来管理实际事务。

目录

1. 引言

intro

TransactionScope 是 .NET Framework 中一个非常特殊且重要的类。支持代码块的事务是该类的主要职责。我们经常使用此类来管理代码中的本地事务和分布式事务。TransactionScope 的使用非常简单明了。它非常可靠且易于使用。因此,它在 .NET 开发人员中非常受欢迎。在本文中,我将通过代码示例解释事务相关的理论,并展示各种可以使用 TransactionScope 及其各种选项来管理实际事务的场景。

2. 背景

对于任何业务应用程序来说,事务管理都非常非常重要。每一个大型开发框架都提供了一个用于管理事务的组件。.NET Framework 是一个大型开发框架,它也提供了自己的事务管理组件。在 .NET Framework 2.0 发布之前,我们使用 SqlTransaction 来管理事务。从 2.0 版本开始,.NET Framework 引入了 TransactionScope 类。这个类位于 System.Transactions 程序集中。这个类提供了一个事务框架,借助该框架,任何 .NET 开发人员都可以编写事务性代码,而无需了解太多细节。因此,它在 .NET 开发人员中非常受欢迎,并且他们广泛使用它来管理事务。但故事还没有结束。我可以说故事才刚刚开始。

在现实世界中,您会遇到一些特殊情况、特殊问题,仅仅了解如何使用 TransactionScope 是不够的。要解决死锁、超时等事务性问题,您必须了解与事务直接/间接相关的每一个概念。别无选择。因此,事务及其相关组件的概念需要清晰。

3. 如何使用 TransactionScope

在 .NET 应用程序中使用 TransactionScope 非常非常简单。任何人都可以按照以下步骤进行操作:

  1. 将 System.Transactions 程序集添加到项目中。
  2. 使用 TransactionScope 类通过 `using` 语句创建一个事务范围/区域。
  3. 编写需要事务支持的代码。
  4. 执行 TransactionScope.Complete 方法来提交并完成事务。

确实,就是这么简单。但在实际项目中,仅了解这些知识是不够的。您需要更多的事务相关知识,否则您将无法处理事务相关的问题。所以,首先,我们应该清楚事务概念。

4. 事务

什么是事务?您可以从各种来源(如维基百科、其他网站、书籍、文章、博客)找到事务的定义。简而言之,我们可以说,一系列被视为一个整体的工作,要么全部完成,要么不完成。

示例:将钱从银行账户 A 转账到账户 B

一系列(实际上是两个)任务/进程

  1. 从账户 A 提取金额
  2. 将该金额存入账户 B

我们理解,从账户 A 到账户 B 的转账包含两个独立的过程。只有当这两个过程都单独成功时,转账才会被认为是准确和成功的。如果不是这样,假设过程 1 成功但过程 2 失败,那么钱将从账户 A 中扣除,但不会存入账户 B。如果发生这种情况,那将非常糟糕,没有人会接受。

5. 业务事务

业务事务是客户/供应商/股东以及其他参与商业活动的其他方之间的交互。在本文中,我将不介绍任何关于业务事务的内容。

6. 数据库事务

在软件开发中,当我们说事务时,我们默认会猜测是数据库事务。在数据库事务中,我们可以说,一系列数据操作语句(insert/update/delete)作为一个整体执行。所有语句要么成功执行,要么所有语句都失败,从而使数据库处于一致状态。数据库事务实际上以准确的方式表示数据库状态的更改。

7. 本地事务

Local Transaction

一种事务,其中一系列数据操作语句在单个数据源/数据库上作为一个整体执行。它实际上是一个由数据库直接处理的单阶段事务。为了管理本地事务,System.Transactions 有一个轻量级事务管理器 (LTM)。它充当网关。由 System.Transactions 启动的所有事务都由该组件直接处理。如果它根据某些预定义规则发现事务性质是分布式的,它会回退到 MSDTC 分布式事务。

8. 分布式事务

Distributed Transaction

一种与多个数据源协同工作的事务称为分布式事务。如果事务失败,受影响的数据源将被回滚。在 System.Transactions 中,MSDTC (Microsoft Distributed Transaction Coordinator) 管理分布式事务。它实现两阶段提交协议。分布式事务比本地事务慢得多。当事务对象意识到需要分布式事务时,它会自动将本地事务升级为分布式事务。开发人员在此处无能为力。

9. 分布式事务系统架构

我们知道在分布式事务中,有多个站点参与。每个站点都有两个组件

  1. 事务管理器
  2. 事务协调器

1. 事务管理器:维护一个日志,并在需要恢复时使用该日志。它通过启动和完成事务来控制整个事务,并管理事务的持久性和原子性。它还协调一个或多个资源上的事务。有两种类型的事务管理器。

  1. 本地事务管理器:仅协调单个资源上的事务。
  2. 全局事务管理器:协调多个资源上的事务。

2. 事务协调器:启动站点上发生的事务的执行。将子事务分发到适当的站点,以便它们可以在这些站点上执行。协调每个站点的每个事务。结果,事务被提交或回滚到所有站点。

10. 连接事务

直接与数据库连接(SqlConnection)绑定的事务称为连接事务。SqlTransaction (IDbTransaction) 是连接事务的一个例子。在 .NET Framework 1.0/1.1 中,我们使用 SqlTransaction

string connString = ConfigurationManager.ConnectionStrings["db"].ConnectionString;
using (var conn = new SqlConnection(connString))
{
    conn.Open();
    using (IDbTransaction tran = conn.BeginTransaction())
    {
        try
        {
            // transactional code...
            using (SqlCommand cmd = conn.CreateCommand())
            {
                cmd.CommandText = "INSERT INTO Data(Code) VALUES('A-100');";
                cmd.Transaction = tran as SqlTransaction;
                cmd.ExecuteNonQuery();
            }
            tran.Commit();
        }
        catch(Exception ex)
        {
            tran.Rollback();
            throw;
        }
    }
}

11. 隐式事务

一种事务,它自动识别需要事务支持的代码块,而无需显式提及任何与事务相关的内容。隐式事务不只是绑定到数据库,任何支持事务的提供程序都可以使用。TransactionScope 实现隐式事务。如果您查看 TransactionScope 的用法,您不会发现任何事务相关的内容被发送到任何方法或设置任何属性。如果代码块属于任何 TransactionScope,则会自动将其与事务关联。WCF 事务是事务感知提供程序的另一个示例。任何人都可以编写像 WCF 实现一样的事务感知提供程序。

12. 事务属性

事务有四个重要属性。我们称之为 ACID 属性。它们是

    1. A-原子性
    2. C-一致性
    3. I-隔离性
    4. D-持久性
  1. 原子性:如果事务的所有部分单独成功,则数据将被提交,数据库将被更改。如果事务的任何一部分失败,那么事务的所有部分都将失败,数据库将保持不变。事务的一部分可能因各种原因而失败,例如业务规则违反、断电、系统崩溃、硬件故障等。
  2. 一致性:事务将遵循各种数据库规则(如各种数据完整性约束(主键/唯一键、检查/非空约束、具有有效引用的引用完整性、级联规则)等),将数据库从一个有效状态更改为另一个有效状态。
  3. 隔离性:一个事务将对另一个事务隐藏。换句话说,我们可以说,如果两个事务并发执行,那么一个事务将不会影响另一个事务。
  4. 持久性:在事务成功完成后(提交到数据库)后,更改的数据在任何情况下(如系统故障、数据库崩溃、硬件故障、断电等)都不会丢失。

13. 事务隔离级别

现在我将开始解释与事务直接相关的非常重要的一点,那就是事务隔离级别。为什么它如此重要?首先,我之前解释过隔离性是重要的事务属性。它描述了每个事务都与其他事务隔离,并且不会影响其他并发执行的事务。事务管理系统如何实现这一重要功能?

事务管理系统引入了锁定机制。借助此机制,一个事务可以与其他事务隔离。锁定策略的行为因每个事务设置的隔离级别而异。.NET 事务范围中有四种非常重要的隔离级别。它们是

    1. 串行化
    2. 可重复读
    3. 读已提交
    4. 读未提交

在开始解释隔离级别之前,我需要解释事务内部的数据读取机制。这种数据读取机制对于正确理解隔离级别非常重要。

  • 脏读:一个事务读取了另一个事务已更改但尚未提交的数据。您可能会根据该数据做出决定/采取行动。当稍后回滚数据时,问题就会出现。如果发生回滚,您的决定/行动将是错误的,并会在您的应用程序中产生 bug。
  • 不可重复读:一个事务多次读取同一表中的同一数据。当每次读取的数据都不同时,问题就会出现。
  • 幻读:假设一个事务首先读取一个表,发现有 100 行。当同一个事务进行另一次读取时,发现有 101 行,问题就会出现。额外的行称为幻行。

现在我将简要解释重要的隔离级别

  1. 串行化:最高级别的隔离。在读写发生时,它会独占锁定数据。它获取范围锁,以防止创建幻行。
  2. 可重复读:第二高级别的隔离。与串行化相同,除了它不获取范围锁,因此可能会创建幻行。
  3. 读已提交:它允许共享锁并仅读取已提交的数据。这意味着永远不要读取事务中间的已更改数据。
  4. 读未提交:它是最低级别的隔离。它允许脏读。

现在我将开始解释 TransactionScope 及其用法模式

14. TransactionScope 默认属性

了解 TransactionScope 对象的默认属性非常重要。为什么?因为很多时候我们创建并使用该对象而不进行任何配置。

三个非常重要的属性是

  1. IsolationLevel
  2. Timeout
  3. TransactionScopeOptions

我们像这样创建和使用 TransactionScope

using (var scope = new TransactionScope())
{
    //transctional code…
    scope.Complete();
} 

这里 TransactionScope 对象是用默认构造函数创建的。我们没有为 IsolationLevelTimeoutTransactionScopeOptions 定义任何值。因此,它会获得这三个属性的默认值。所以现在我们需要知道这些属性的默认属性值是什么。

属性 默认值 可用选项
IsolationLevel串行化Serializable, Read Committed, Read Un Committed, Repeatable Read
Timeout1 分钟最多 10 分钟
TransactionScopeOption必需Required, Required New, Suppress
  1. 隔离级别:它定义了事务内部数据读取的锁定机制和策略。
  2. 超时:对象将等待事务完成多长时间。切勿将其与 SqlCommandTimeout 属性混淆。SqlCommand Timeout 定义 SqlCommand 对象等待数据库操作(select/insert/update/delete)完成多长时间。
  3. TransactionScopeOption:这是一个枚举。此枚举中有三个可用选项
选项 描述
1 必需 这是 TransactionScope 的默认值。如果已存在任何事务,它将加入该事务,否则创建新事务。
2 RequiredNew 选择此选项时,始终会创建一个新事务。此事务与其外部事务无关。
3 Suppress选择此选项时,不会创建任何事务。即使已经

如何知道这些属性的默认值?

System.Transactions 程序集有两个类

  1. 事务
  2. TransactionManager

这些类将提供默认值。在 TransactionScope 内部,如果您运行以下代码,您将知道默认值:

using (var scope = new System.Transactions.TransactionScope())
{
    IsolationLevel isolationLevel = Transaction.Current.IsolationLevel;
    TimeSpan defaultTimeout = TransactionManager.DefaultTimeout;
    TimeSpan maximumTimeout = TransactionManager.MaximumTimeout;
}

是否可以覆盖默认属性值?

是的,您可以。假设您希望默认值为 30 秒,最大超时值为 20 分钟。如果这是要求,则可以使用 web.config 来实现。

<system.transactions>    
    <defaultSettings timeout="30"/>
    <machineSettings maxTimeout="1200"/>
</system.transactions>
对于 machineSettings 值,您需要更新服务器上的 machine.config 文件。
<section name="machineSettings" type="System.Transactions.Configuration.MachineSettingsSection,
System.Transactions,Version=2.0.0.0,Culture=neutral,PublicKeyToken=b77a5c561934e089,
Custom=null"allowdefinition="MachineOnly"allowexedefinition="MachineToApplication" />

15. 事务隔离级别的选择

在使用隔离级别时,您需要有正确的知识。下表将为您提供一个基本概念,以便您可以理解基础知识并为您的事务范围选择合适的隔离级别。

隔离级别 建议
串行化 它在读/写操作时独占锁定数据。因此,很多时候它可能会导致死锁,从而导致超时异常。您可以将此隔离级别用于高度安全的事务性应用程序,例如金融应用程序。
可重复读 与串行化相同,除了允许幻行。可用于金融应用程序或重事务性应用程序,但需要知道幻行创建场景不存在的地方。
读已提交 大多数应用程序都可以使用它。SQL Server 的默认隔离级别是这个。
读未提交 具有这些的应用程序无需支持并发事务。

现在我将通过场景解释如何使用 TransactionScope

16. 要求-1

创建一个隔离级别为读已提交且事务超时为 5 分钟的事务。

实现

var option = new TransactionOptions();
option.IsolationLevel = IsolationLevel.ReadCommitted;
option.Timeout = TimeSpan.FromMinutes(5);
using (var scope = new TransactionScope(TransactionScopeOption.Required, option))
{
    ExcuteSQL("CREATE TABLE MyNewTable(Id int);");                                        
    scope.Complete();
} 

首先,创建 TransactionOptions 并将其 IsolationLevelTimeout 属性分别设置为 ReadCommitted 和 5 分钟。

其次,通过创建具有参数化构造函数的 TransactionScope 对象来创建一个事务块。在此构造函数中,您将传递一个您之前创建的 TransactionOptions 对象以及 TransactionScopeOption.Required 值。

一个重要的注意事项:很多时候我们在事务中使用 DDL(数据定义语言)语句时会感到困惑,并且会提出一个问题:它是否支持 DDL 事务?答案是肯定的。您可以在事务中使用 DDL 语句,如 create/alter/drop 语句。您甚至可以在事务中使用 Truncate 语句。

17. 要求-2

我们需要创建一个事务,其中一个数据库操作将在我的本地数据库中,而另一个将在远程数据库中。

实现

using (var scope = new TransactionScope())
{
    UpdateLocalDatabase();
    UpdateRemoteDatabase();
    scope.Complete();
} 

本地或远程/分布式事务的实现代码在事务中没有区别。之前我说过 TransactionScope 实现隐式事务类型。这意味着它会自动标记需要事务支持的代码块,无论是本地还是远程。但是,在处理分布式事务时,您可能会遇到错误。错误消息将如下所示:

"伙伴事务管理器已禁用对远程/网络事务的支持。"

如果您遇到这种类型的异常,您需要配置 MSDTC 的安全设置,包括本地和远程服务器,并确保服务正在运行。

要查找 MSDTC 配置界面,请转到

控制面板 > 管理工具 > 组件服务 > 分布式事务协调器 > LocalDTC

安全选项卡的一些选项描述如下:

属性名称 描述
网络 DTC 访问 如果未选中,MSDTC 将不允许任何远程事务
允许远程客户端 如果选中此项,MSDTC 将允许为事务协调远程客户端。
允许远程管理 允许远程计算机访问和配置这些设置。
允许入站 允许计算机将事务流向本地计算机。当 MSDTC 作为资源管理器(如 SQL Server)托管时,需要此选项。
允许出站 允许计算机将事务流向远程计算机。这对于发起事务的客户端计算机是必需的。
相互身份验证 本地和远程计算机使用加密消息进行通信。它们使用 Windows 域帐户建立安全连接进行消息通信。
需要传入呼叫身份验证 如果无法建立相互身份验证,但传入的呼叫者已通过身份验证,则将允许通信。它仅支持 Windows 2003/XP ServicePack-2。
无需身份验证 它允许任何未经身份验证的非加密通信。

启用 XA 事务 允许不同的操作系统使用 XA 标准与 MSDTC 进行通信。
DTC 登录帐户 DTC 服务运行帐户。默认帐户是 Network Service。

18. 分布式事务性能

分布式事务比本地事务慢。两阶段提交协议用于管理分布式事务。两阶段提交协议只不过是一种执行分布式事务的算法。主要使用三种提交协议

  1. 自动提交:如果所有 SQL 语句都成功执行,则事务会自动提交;如果其中任何一条语句执行失败,则会回滚。
  2. 两阶段提交:事务在最终提交前等待所有参与事务的其他方的消息。它在提交或回滚之前锁定资源。因此,它被称为阻塞协议。在性能方面,它是其速度慢得多的原因。它是管理分布式事务的常用协议。
  3. 三阶段提交:如果所有节点都同意,则事务最终提交。它是一种非阻塞协议。在性能方面,它比两阶段提交协议更快。该协议复杂且昂贵,但避免了两阶段提交协议的一些缺点。

19. 要求-3

我想在另一个事务中创建一个事务。

实现

string connectionString = ConfigurationManager.ConnectionStrings["db"].ConnectionString;
var option = new TransactionOptions
{
     IsolationLevel = IsolationLevel.ReadCommitted,
     Timeout = TimeSpan.FromSeconds(60)
};
using (var scopeOuter = new TransactionScope(TransactionScopeOption.Required, option))
{
    using (var conn = new SqlConnection(connectionString))
    {
        using (SqlCommand cmd = conn.CreateCommand())
        {
            cmd.CommandText="INSERT INTO Data(Code, FirstName)VALUES('A-100','Mr.A')";
            cmd.Connection.Open();
            cmd.ExecuteNonQuery();
        }
    }
    using (var scopeInner = new TransactionScope(TransactionScopeOption.Required, option))
    {
        using (var conn = new SqlConnection(connectionString))
        {
            using (SqlCommand cmd = conn.CreateCommand())
            {
                cmd.CommandText="INSERT INTO Data(Code, FirstName) VALUES('B-100','Mr.B')";
                cmd.Connection.Open();
                cmd.ExecuteNonQuery();
            }
        }
        scopeInner.Complete();
    }
    scopeOuter.Complete();
}

在另一个(嵌套)事务中创建事务没有问题。您应该定义内部事务的行为。这种行为取决于 TransactionScopeOption 的值。如果您选择 Required 作为 TransactionScopeOption,它将加入其外部事务。这意味着如果外部事务已提交,则内部事务将提交;如果外部事务已回滚,则内部事务将回滚。如果您选择 TrasnactionScopeOptionRequiredNew 值,则会创建一个新事务,并且此事务将独立提交或回滚。在处理 TransactionScope 的嵌套事务之前,您必须清楚这些概念。

20. 要求-4

我想从事务中显式调用回滚。

实现

using (var scope = new TransactionScope())
{
    //either 1 of following lines will use
    Transaction.Current.Rollback();
    scope.Dispose();
    //if you comment the following line transaction will
    //automatically be rolled back
    //scope.Complete();
}

如果您不调用 TransactionScope.Complete() 方法,则事务将自动回滚。如果您需要为某些场景显式调用回滚,则有两种选择:

  1. 执行 Transaction.Current.Rollback() 将回滚当前事务。
  2. 执行 TransactionScope.Dispose() 也将回滚当前事务。

只有一件事:请记住,如果您显式调用 Transaction.Rollback()TranactionScope.Dispose(),则不应调用 TransactionScope.Complete() 方法。如果您这样做,您将收到一个 ObjectDisposeException

"无法访问已释放的对象。对象名称:'TransactionScope'"

21. 要求-5

我想在事务范围内动态创建一个文件/文件夹。如果我的事务被回滚,我希望创建的文件/文件夹被自动删除,就像数据库行一样。

实现

string newDirectory = @"D:\TestDirectory";
string connectionString = ConfigurationManager.ConnectionStrings["db"].ConnectionString;
using (var scope = new TransactionScope())
{
    using (var conn = new SqlConnection(connectionString))
    {
        using (SqlCommand cmd = conn.CreateCommand())
        {
            cmd.CommandText = "Insert into data(Code) values ('A001');";
            cmd.Connection.Open();
            cmd.ExecuteNonQuery();
        }
    }
    Directory.CreateDirectory(newDirectory);
    File.Create(@"D:\NewFile.txt").Dispose();
    scope.Dispose();
} 

TranactionScope 不仅限于数据库。它支持文件系统、MSMQ 等其他数据源。但您需要更多工作来支持 TranactionScope。首先,我在上面的代码块中显示的 **将不起作用**。为什么?因为默认情况下,目录创建和文件创建不会被标记为事务。那么我们需要做什么?

public interface IEnlistmentNotification
{       
    void Commit(Enlistment enlistment);       
    void InDoubt(Enlistment enlistment);      
    void Prepare(PreparingEnlistment preparingEnlistment);        
    void Rollback(Enlistment enlistment);
} 

System.Transactions 命名空间有一个名为 IEnlistmentNotification 的接口。如果我希望我的组件/服务支持事务,我需要实现该接口。以下代码将展示一种非常简单直接的实现方式:

public class DirectoryCreator : IEnlistmentNotification
{
    public string _directoryName; 
    private bool _isCommitSucceed = false;
    public DirectoryCreator(string directoryName)
    {
        _directoryName = directoryName;
        Transaction.Current.EnlistVolatile(this, EnlistmentOptions.None);
    }
    public void Commit(Enlistment enlistment)
    {
        Directory.CreateDirectory(_directoryName);
        _isCommitSucceed = true;
        enlistment.Done();
    }
    public void InDoubt(Enlistment enlistment)
    {
        enlistment.Done();
    }
    public void Prepare(PreparingEnlistment preparingEnlistment)
    {
        preparingEnlistment.Prepared();
    }
    public void Rollback(Enlistment enlistment)
    {
        if (_isCommitSucceed))
            Directory.Delete(_directoryName);
        enlistment.Done();
    }
} 

上面的类将创建一个目录(文件夹),并且该组件支持事务。我们可以将此类与任何 TranactionScope 一起使用,如果 TranactionScope 已提交,则目录将被创建;否则,它将被删除(如果已创建)。我这里只展示了目录的创建,如果您愿意,可以创建一个用于文件创建的类/组件。那么,如何在事务范围中使用这个类呢?

string newDirectory = @"D:\TestDirectory";
string connectionString = ConfigurationManager.ConnectionStrings["db"].ConnectionString;
using (var scope = new TransactionScope())
{
    using (var conn = new SqlConnection(connectionString))
    {
        using (SqlCommand cmd = conn.CreateCommand())
        {
            cmd.CommandText = "Insert into data(Code) values ('A001');";
            cmd.Connection.Open();
            cmd.ExecuteNonQuery();
        }
    }
    var creator = new DirectoryCreator(newDirectory);
    Transaction.Current.Rollback();
    //scope.Complete();
}

现在,它将起作用!

Transactional NTFS(TxF) .NET 是一个开源项目。您可以使用此库在 transactionscope 中创建/写入/复制文件/目录,它将自动支持事务。

  • 您首先需要从 http://txfnet.codeplex.com 下载组件。
  • 将该组件作为引用添加到您的项目中。
  • 在您的事务块中使用组件 API。

TxF API 用法代码示例

using (var ts = new System.Transactions.TransactionScope())
{
    using (var conn = new SqlConnection(connectionString))
    {
        using (SqlCommand cmd = conn.CreateCommand())
        {
            cmd.CommandText = "Insert into data(Code) values ('A001');";
            cmd.Connection.Open();
            cmd.ExecuteNonQuery();
        }
    }
    TxF.Directory.CreateDirectory("d:\\xyz", true);
    TxF.File.CreateFile("D:\\abc.txt", File.CreationDisposition.OpensFileOrCreate);
    ts.Complete();
}

TxF 组件支持

  • 创建/删除目录
  • 创建/删除文件
  • 读取/写入文件
  • 复制文件

22. 兴趣点

事务管理实际上是一个庞大的主题。它也是一个非常复杂的主题,特别是分布式事务。我已尽力将其呈现得尽可能简单。如果您想获得所有事务相关的知识,您应该对此进行更多研究。我建议您阅读有关事务的研究论文,特别是分布式事务。

您还可以探索更多关于事务感知服务/组件的内容。我在这里展示了一种非常简单的实现方式。但在实际生活中,您可能会面临困难的场景。所以你需要为此做好准备。不久的将来,微软可能会添加事务感知组件,如字典/文件系统/目录服务等。如果发生这种情况,开发人员的生活将会更加轻松。

示例源代码

我已将源代码示例附加到本文中。我使用 Visual Studio 2012 和 .NET Framework 4.5 编写了此源代码。我添加了一个单元测试项目,以便您可以调试/测试代码并正确理解它。

参考文献

© . All rights reserved.