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

ADO.NET 事务和 TransactionScope 入门教程

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (33投票s)

2013年1月5日

CPOL

8分钟阅读

viewsIcon

157071

downloadIcon

2359

本文是一篇入门教程,旨在帮助您理解什么是事务以及如何在 .Net 框架和 ADO.NET 中为任何 ASP.NET Web 应用程序或其他数据库驱动的应用程序实现事务。

引言

本文是一篇入门教程,旨在帮助您理解什么是事务以及如何在 .Net 框架和 ADO.NET 中为任何 ASP.NET Web 应用程序或其他数据库驱动的应用程序实现事务。

背景 

在日常语境中,“交易”一词表示以等价物交换商品、产品或金钱。使这种交换成为交易的事实是,商品或金钱的总量保持不变,即不会增加或减少,并且可靠性,即如果一方付出某物,则另一方将接收相同数量(不多不少)。

同样,当我们谈论数据库操作中的事务时,我们执行某些数据库操作,使得所有数据库操作要么成功,要么全部失败。这将导致在事务完成或失败后信息总量保持不变。

为了说明上述过程,假设我有两个账户持有人,一个人正试图将一些钱转给另一个人。从数据库的角度来看,此操作包含两个子操作,即:

  1. 从第一个账户中扣除指定金额。
  2. 其次,将所需金额存入第二个账户。

现在从技术角度来看,如果第一个操作成功而第二个操作失败,结果将是第一个人的账户将被扣款,而第二个账户将不会被记账,即我们丢失了信息量。反之,第二个账户将增加金额,而第一个账户甚至不会被扣款。

所以这里的关键是我们既需要两者都成功,也需要两者都失败。任何一个操作的成功都会导致结果不一致,因此即使一个操作失败,我们也需要回滚在另一个操作中所做的更改。这正是事务的用处所在。

现在让我们看一下与 .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 对象。为此,应执行以下步骤:

  1. 创建连接。
  2. 创建事务。
  3. 为事务内的所有操作创建命令。
  4. 打开连接。
  5. 开始事务。
  6. 将所有命令与上述事务对象关联。
  7. 执行命令。
  8. 单独检查命令状态。
  9. 如果任何命令失败,则回滚事务。
  10. 如果所有命令都成功,则提交事务。

为了说明上述过程,让我们编写一些代码来在我们的示例应用程序中执行事务。

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 对象处理事务,需要执行以下操作:

  1. 在一个 using 块中创建一个 TransactionScope 对象。
  2. 在此 TransactionScope using 块中创建连接。
  3. 在此处创建所有命令。
  4. 使用命令执行所有操作。
  5. 如果所有操作都成功,则调用 TransactionScope 对象上的 Complete 函数。
  6. 如果任何命令失败,让控制流跳出作用域,事务将被回滚。

为了说明这个过程,让我们尝试使用 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 产品不了解 LTMDTC。因此,对于早期产品,ADO.NET DbTransaction 对象是从代码处理事务的理想方式。

关注点

在本文中,我们尝试从非常初学者的角度探讨什么是事务以及如何在 .NET 中处理事务。从主题的角度来看,这仅仅是冰山一角。我们没有讨论隔离级别和分布式事务(这本身就是一个庞大的主题)。但我们确实讨论了在 .NET 中处理事务的各种方法。

历史

  • 2013 年 1 月 5 日:初稿。
© . All rights reserved.