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

C#/SQL 中的可提交事务

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (10投票s)

2010年1月30日

CPOL

4分钟阅读

viewsIcon

62813

downloadIcon

1049

简单的控制台应用程序,展示了如何使用 .NET 的 CommittableTransaction 类

引言

在开发订单管理系统的过程中,出现了对交易记录进行事务性处理的需求。这个测试应用程序以简化的方式展示了如何修改 SQL Server 中表的内容,并利用该修改的结果来更新另一个依赖的表。

背景

在 SQL 中,通常的做法是将记录插入到表中,并检索插入行的标识(使用 RETURN @@IDENTITY),然后使用这些标识来对另一个表执行其他相关的插入操作。

显然,如果第一批事务抛出错误,你可能无法提交第二批相关的事务。反过来说,第二批(相关的)事务也可能抛出错误。在这种情况下,你已经将数据提交到了第一个表,并且可能希望回滚这些更改,以确保数据库不会处于不一致的状态。

这在我要解决的业务场景中尤为重要。对于不熟悉金融领域的人来说,大多数交易都起源于“执行”(可以想象交易员打电话给经纪人说:“嘿,买 100 股 IBM 股票”),最终被“分配”到多个基金(这意味着 20 股给一个基金,30 股给另一个,50 股给另一个)。在设计存储这些交易信息的数据库时,很明显,如果使用简单的外键关系连接父“执行”表和“已分配交易”子表,可以轻松完成设置。在此场景中有两个业务需求。首先,如果执行未能进入执行表(无论什么原因),那么从该执行分配的任何交易都不应进入数据库。这很容易实现,因为如果执行插入失败,你将无法获得有效的 executionid 来插入到第二个表中,结果是外键约束将不允许你插入已分配的交易。另一个需求与第一个情况相反,如果任何已分配的交易未能进入数据库(无论什么原因),那么原始执行也不应在数据库中。在这种情况下,外键约束将无关紧要,因为是子表未能正确更新,因此需要一种机制来回滚原始父表插入,以防后续相关的事务失败。这时就可以利用 .NET 的 CommittableTransaction 类了。

Using the Code

CommittableTransactionTester 是一个直接的控制台应用程序,它会更新数据库,并根据批处理是否失败来回滚或提交事务。请注意,我已经省略了 Main() 中调用的一些方法,可以在附带的项目中找到它们以供参考。还有一个 TEST.bak 文件,在运行测试应用程序之前应该将其加载到 SQL Server 中。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.SqlClient;
using System.Transactions;

namespace CommittableTransactionTester
{
  class Program
  {
     private static SqlConnection conn;
     static void Main(string[] args)
     {
       //This instance of CommittableTransaction can be shared 
       //across multiple method calls.
       //Note that the related class, SqlTransaction, 
       //can only be associated with a single SqlCommand object
       CommittableTransaction MASTER_TRANSACTION = new CommittableTransaction();
       //Obviously, you may have to alter your connection string 
       conn = new SqlConnection(string.Format(@"Server={0};DataBase={1};
			Trusted_Connection={2}",
                           "LOCALHOST", "TEST", "True"));

       conn.Open();
       //This lets the db connection know that you are about to perform a transaction
       //Note that in a multi-user environment, 
       //this can cause your database to hang if you don't specify your
       //IsolationLevel during db reads (I'll try to post an example of how to avoid this)
       conn.EnlistTransaction(MASTER_TRANSACTION);
       try
       {
          Console.WriteLine("Enter an integer value to be inserted into table 2:");
          int table2Input = Int32.Parse(Console.ReadLine());
          Console.WriteLine("Enter an integer value to be inserted into table 1:");
          int table1Input = Int32.Parse(Console.ReadLine());
          //See the attached code sample for UpdateTable2 method
          UpdateTable2(table2Input, "UPDATING TABLE 2", MASTER_TRANSACTION);
          //nullable, just in case...
          int? readTable2 = ReadTable2(table2Input, MASTER_TRANSACTION);
          //See the attached code sample for UpdateTable1 method	
          UpdateTable1((int)table1Input, (int)readTable2, 
          string.Format("PASS! VALUE {0} IN TEST_TABLE_2", readTable2), 
						MASTER_TRANSACTION);
          //Attempt to commit your changes, 
          //any errors that occurred during update methods 
          //should cause this to throw an error 
          //(whether they were rolled back in your method call or not)
          MASTER_TRANSACTION.Commit();

          Console.WriteLine("TRANSACTIONS COMMITTED SUCCESSFULLY!");
       }
       catch (System.Transactions.TransactionException tranEx)
       {
	 //you really only need to roll back here 
          //if the transaction has not been aborted
          //in this particular app, the transaction has 
          //already been rolled back/aborted.
          if(!MASTER_TRANSACTION.TransactionInformation.Status.Equals
		(TransactionStatus.Aborted))
              MASTER_TRANSACTION.Rollback(tranEx);

          Console.WriteLine(tranEx.Message);
       }
       catch (Exception ex)
       {
           //Just in case some other, non-transactional error has occurred, 
           //you may want to rollback
           MASTER_TRANSACTION.Rollback();
           Console.WriteLine(ex.Message);
       }
       Console.ReadKey();
    }
 }
}    

代码应该相对直接(如果不是,那也没办法,这是我第一次尝试写 CodeProject 文章!)。第一次运行项目时不应出现任何错误,无论你的输入是什么。

当进行后续测试时,你才会开始注意到 CommittableTransaction 的强大之处。例如,如果你将值 1 和 1 插入到表中,然后尝试再次传入相同的值,你肯定会收到一个错误。

另一个测试(假设我一开始就从 1,1 开始)是检查你尝试插入值 2 和 1 的情况。在这种情况下,将值 2 插入到表 2 是完全可以的(假设你还没有这样做!),但你绝对不能将值 1 插入到表 1(假设你已经这样做了!)。因此,你可能希望回滚将值 2 插入到表 2 的操作,而这正是发生的。

这里的重点是,第一个事务(将值 2 插入到表 2)并未失败,而是尝试将值 1 插入到第一个表时失败了,因此值 2从未进入第二个表(因为整个事务都被回滚了)。

关注点

在实际应用中,我在我们每天结束时的交易处理过程中,将 CommittableTransaction 类的实例在各种不同的方法调用之间共享。整个过程作为一个批处理执行,如果交易未能进入,则回滚所有操作;如果我们未获得某个证券的价格(这对这里未描述的其他计算是必需的),则回滚所有操作……你明白我的意思。在这些方法中的每一个中,都有成百上千个 insert/update 过程需要成功执行,才能使我们的数据保持一致状态。如果其中任何一个过程失败,它们都会被回滚。

历史

  • 原始发布日期:2010 年 1 月 29 日
© . All rights reserved.