ADO.NET 事务和 TransactionScope 入门教程






4.80/5 (33投票s)
本文是一篇入门教程,旨在帮助您理解什么是事务以及如何在 .Net 框架和 ADO.NET 中为任何 ASP.NET Web 应用程序或其他数据库驱动的应用程序实现事务。
引言
本文是一篇入门教程,旨在帮助您理解什么是事务以及如何在 .Net 框架和 ADO.NET 中为任何 ASP.NET Web 应用程序或其他数据库驱动的应用程序实现事务。
背景
在日常语境中,“交易”一词表示以等价物交换商品、产品或金钱。使这种交换成为交易的事实是,商品或金钱的总量保持不变,即不会增加或减少,并且可靠性,即如果一方付出某物,则另一方将接收相同数量(不多不少)。
同样,当我们谈论数据库操作中的事务时,我们执行某些数据库操作,使得所有数据库操作要么成功,要么全部失败。这将导致在事务完成或失败后信息总量保持不变。
为了说明上述过程,假设我有两个账户持有人,一个人正试图将一些钱转给另一个人。从数据库的角度来看,此操作包含两个子操作,即:
- 从第一个账户中扣除指定金额。
- 其次,将所需金额存入第二个账户。
现在从技术角度来看,如果第一个操作成功而第二个操作失败,结果将是第一个人的账户将被扣款,而第二个账户将不会被记账,即我们丢失了信息量。反之,第二个账户将增加金额,而第一个账户甚至不会被扣款。
所以这里的关键是我们既需要两者都成功,也需要两者都失败。任何一个操作的成功都会导致结果不一致,因此即使一个操作失败,我们也需要回滚在另一个操作中所做的更改。这正是事务的用处所在。
现在让我们看一下与 .Net 框架中的事务和事务相关的一些技术细节。然后我们将看到如何在 .NET 中实现事务。
使用代码
根据定义,事务必须是原子性 (Atomic)、一致性 (Consistent)、隔离性 (Isolated) 和持久性 (Durable) 的。这些术语是什么意思?
事务的属性
- 原子性:原子性意味着构成事务的所有语句(SQL语句或操作)应作为一个原子操作工作,即要么全部成功,要么全部失败。
- 一致性:这意味着如果原子事务成功,数据库应处于反映更改的状态。如果事务失败,数据库应与事务开始时完全相同。
- 隔离性:如果有多个事务正在进行,那么每个事务都应独立工作,不应影响其他事务。
- 持久性:持久性意味着一旦事务提交,更改就应该是永久的,即这些更改将保存在数据库中,并且无论发生什么(如断电或其他情况)都会保留。
示例代码说明
现在为了说明如何实现事务,我们将使用一个包含单个表数据库的小型应用程序。该表包含账户 ID 和账户中的金额。该应用程序将促进账户之间的金额转账。由于涉及两个操作,我们将看到如何使用事务来执行这些操作。
示例数据库表如下所示:

用户界面将如下所示:

我们将实现此页面的三个版本,以展示三种不同的方法。我们将看到如何使用 SQL 事务在数据库级别处理事务,如何使用 ADO.NET 事务对象实现事务,最后,我们将看到如何使用 TransactionScope 对象实现事务。
注意:本文中的代码是为了阐述事务功能而编写的,不符合编码标准,容易受到 SQL 注入攻击。不应将其视为可用于生产环境的代码。这仅仅是示例代码,还有很大的改进空间。
在 SQL 中创建和实现事务
事务也可以在 SQL 级别处理。用于处理事务的 SQL 结构如下:
BEGIN TRY
    BEGIN TRANSACTION
        -- update first account and debit from it
        -- update second account and credit in it
    COMMIT TRANSACTION
END TRY
BEGIN CATCH
    ROLLBACK TRANSACTION
END CATCH
因此,如果出现问题,SQL 本身将负责回滚事务。
因此,对于我们的示例应用程序,我们可以编写以下代码来使此代码在启用 SQL 事务的情况下工作。
private void PerformTransaction(string creditAccountID, string debitAccountID, double amount)
{        
    SqlConnection con = null;
    // they will be used to decide whether to commit or rollback the transaction
    bool result = false;
   
    string updateCommand = @"BEGIN TRY
                                BEGIN TRANSACTION
                                    update Account set Amount = Amount - {0} where ID = {1}
                                    update Account set Amount = Amount + {2} where ID = {3}
                                COMMIT TRANSACTION
                            END TRY
                            BEGIN CATCH
                                ROLLBACK TRANSACTION
                            END CATCH";
    try
    {
        con = new SqlConnection(CONNECTION_STRING);
        con.Open();            
        // Let us do a debit first
        using (SqlCommand cmd = con.CreateCommand())
        {
            cmd.CommandType = CommandType.Text;
            cmd.CommandText = string.Format(updateCommand,                    
                amount, debitAccountID,
                amount, creditAccountID);
            // check if 2 records are effected or not
            result = cmd.ExecuteNonQuery() == 2;
        }
    }
    catch
    {
        // do application specific cleanup or show message to user about the problem
    }
    finally
    {
        con.Close();
    }
}
注意:此示例代码仅展示处理事务和数据库操作的代码片段,要获得完整的理解,请参阅示例代码。
使用 ADO.NET DbTransaction 对象创建和实现事务
现在,在 SQL 级别进行事务处理是完美的解决方案,前提是所有操作都在一个地方进行。我可以创建一个存储过程来处理所有事务。但是,如果所有操作都发生在不同的类甚至不同的程序集中,情况会怎样?在这种情况下,需要使用 ADO.NET 事务在代码中处理事务。
处理事务的另一种方法是从代码本身使用 ADO.NET DbTransaction 对象。为此,应执行以下步骤:
- 创建连接。
- 创建事务。
- 为事务内的所有操作创建命令。
- 打开连接。
- 开始事务。
- 将所有命令与上述事务对象关联。
- 执行命令。
- 单独检查命令状态。
- 如果任何命令失败,则回滚事务。
- 如果所有命令都成功,则提交事务。
为了说明上述过程,让我们编写一些代码来在我们的示例应用程序中执行事务。
private void PerformTransaction(string creditAccountID, string debitAccountID, double amount)
{
    SqlTransaction transaction = null;
    SqlConnection con = null;
    // they will be used to decide whether to commit or rollback the transaction
    bool debitResult = false;
    bool creditResult = false;
    try
    {
        con = new SqlConnection(CONNECTION_STRING);
        con.Open();
        // lets begin a transaction here
        transaction = con.BeginTransaction();
        // Let us do a debit first
        using (SqlCommand cmd = con.CreateCommand())
        {
            cmd.CommandType = CommandType.Text;
            cmd.CommandText = string.Format(
                "update Account set Amount = Amount - {0} where ID = {1}",
                amount, debitAccountID);
            // assosiate this command with transaction
            cmd.Transaction = transaction;
            debitResult = cmd.ExecuteNonQuery() == 1;
        }
        // A dummy throw just to check whether the transaction are working or not
        //throw new Exception("Let see..."); // uncomment this line to see the transaction in action
        // And now do a credit
        using (SqlCommand cmd = con.CreateCommand())
        {
            cmd.CommandType = CommandType.Text;
            cmd.CommandText = string.Format(
                "update Account set Amount = Amount + {0} where ID = {1}",
                amount, creditAccountID);
            // assosiate this command with transaction
            cmd.Transaction = transaction;
            creditResult = cmd.ExecuteNonQuery() == 1;
        }
        if (debitResult && creditResult)
        {
            transaction.Commit();
        }
    }
    catch
    {
        transaction.Rollback();            
    }
    finally
    {
        con.Close();
    }
}
此代码块确保与事务关联的所有操作要么成功,要么都不成功。
注意:此示例代码仅展示处理事务和数据库操作的代码片段,要获得完整的理解,请参阅示例代码。
使用 TransactionScope 对象创建和实现事务
使用 ADO.NET 事务创建和使用事务没有问题,只要我们对事务对象执行提交或回滚操作。如果我们忘记这样做并离开代码,就会导致一些问题。
为了解决这些问题,还有另一种处理事务的方法,即使用 TransactionScope 对象。TransactionScope 在处理事务时更像是一种语法糖。它还能确保,如果事务未提交且代码超出作用域,则事务将被回滚。
要使用 TransactionScope 对象处理事务,需要执行以下操作:
- 在一个 using 块中创建一个 TransactionScope对象。
- 在此 TransactionScopeusing 块中创建连接。
- 在此处创建所有命令。
- 使用命令执行所有操作。
- 如果所有操作都成功,则调用 TransactionScope对象上的 Complete 函数。
- 如果任何命令失败,让控制流跳出作用域,事务将被回滚。
为了说明这个过程,让我们尝试使用 TransactionScope 对象在我们的示例应用程序中重新实现该功能。
private void PerformTransaction(string creditAccountID, string debitAccountID, double amount)
{
    // they will be used to decide whether to commit or rollback the transaction
    bool debitResult = false;
    bool creditResult = false;
    try
    {
        using (TransactionScope ts = new TransactionScope())
        {
            using (SqlConnection con = new SqlConnection(CONNECTION_STRING))
            {
                con.Open();
                // Let us do a debit first
                using (SqlCommand cmd = con.CreateCommand())
                {
                    cmd.CommandType = CommandType.Text;
                    cmd.CommandText = string.Format(
                        "update Account set Amount = Amount - {0} where ID = {1}",
                        amount, debitAccountID);
                    debitResult = cmd.ExecuteNonQuery() == 1;
                }
                // A dummy throw just to check whether the transaction are working or not
                throw new Exception("Let see..."); // uncomment this line to see the transaction in action
                // And now do a credit
                using (SqlCommand cmd = con.CreateCommand())
                {
                    cmd.CommandType = CommandType.Text;
                    cmd.CommandText = string.Format(
                        "update Account set Amount = Amount + {0} where ID = {1}",
                        amount, creditAccountID);
                    creditResult = cmd.ExecuteNonQuery() == 1;
                }                                       
                if (debitResult && creditResult)
                {
                    // To commit the transaction 
                    ts.Complete();
                }
            }
        }
    }
    catch
    {
        // the transaction scope will take care of rolling back
    }  
}
此代码块确保 TransactionScope 作用域内的所有操作要么成功,要么都不成功。
注意:此示例代码仅展示处理事务和数据库操作的代码片段,要获得完整的理解,请参阅示例代码。
为了给开发人员提供更多控制,除了 TransactionScope 的作用域控制外,还有一些选项可以与 TransactionScope 关联。
- 
Required:如果当前作用域内已实例化另一个TransactionScope,则此TransactionScope对象将加入其中。
- 
RequiresNew:即使当前作用域内已实例化另一个 TransactionScope,此TransactionScope对象也将创建一个新的事务,该事务将在其作用域内工作。
- 
Supress:即使当前作用域内已实例化另一个TransactionScope,此TransactionScope对象现在也会将作用域内的所有操作排除在现有事务之外。
这些选项可以通过以下方式传递到 TransactionScope 的构造函数中: 
using (TransactionScope ts = new TransactionScope(TransactionScopeOption.Required))
{
    // Code goes here
}
关于分布式事务的说明
如果一个事务跨越多个进程,那么它就是一个分布式事务。也就是说,如果我需要在 SqlServer 中执行一个操作,在 Oracle 中执行另一个操作,并且有一个事务与之关联,那么它将是一个分布式事务。
这很重要,原因有两个。如果事务不是分布式事务,事务的隔离性将由 LTM(轻量级事务管理器)来保证。但如果它是分布式事务,那么分布式事务控制器(DTC)将接管控制。
当我们使用 TransactionScope 类时,如果事务在一个进程中开始,LTM 将继续处理;但如果另一个进程中的另一个操作完成,DTC 将接管,事务随后将自动提升为分布式事务。因此,TransactionScope 类可以轻松创建可提升的事务。
但是,即使对于非分布式事务,也应仅将 transactionScope 对象与 SqlServer 2005 或更高版本一起使用,因为早期 SqlServer 产品不了解 LTM 和 DTC。因此,对于早期产品,ADO.NET DbTransaction 对象是从代码处理事务的理想方式。
关注点
在本文中,我们尝试从非常初学者的角度探讨什么是事务以及如何在 .NET 中处理事务。从主题的角度来看,这仅仅是冰山一角。我们没有讨论隔离级别和分布式事务(这本身就是一个庞大的主题)。但我们确实讨论了在 .NET 中处理事务的各种方法。
历史
- 2013 年 1 月 5 日:初稿。


