C#/SQL 中的可提交事务






4.71/5 (10投票s)
简单的控制台应用程序,展示了如何使用 .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 日