System.Transactions.TransactionScope为了提供一种非常方便的实现分布式事务的方式,但是在某些情况下为了阻止本地事务向分布式事务提升,我们只能通过DbTransaction进行事务控制。在进行数据库操作的时候,我们经常会遇到这样的事务场景:当某个方法单独执行的时候,它应该在一个单一的DbTransaction中执行;当多个方法一起执行的时候,它们应用共同在DbTransaction中执行。在本篇文章中我们通过对DbTransaction进行封装,提供一种类似于TransactionScope的编程方式来解决这个问题。[源代码从这里下载]
目录 一、自定义的Transaction 二、自定义CommittableTransaction和DependentTransaction 三、自定义TransactionScope 四、一个具有事务感知的DbHelper 五、三种事务控制的性能
一、自定义的Transaction
我们完全采用System.Transactions的应用编程接口的设计定义基于DbTransaction的相关类型,首选来看看具有如下定义的表示事务的Transaction类型。Transaction是一个抽象类,具有DbTransactionWrapper和Completed两个属性,前者表示对DbTransaction的封装,后者表示事务操作是否已经完成。静态属性Current表示当前事务,这是一个基于当前线程的静态字段。Rollback和Dispose直接调用了DbTransactionWrapper的同名方法。
代码语言:js复制 1: namespace Artech.Transactions
2: {
3: public class DbTransactionWrapper: IDisposable
4: {
5: public DbTransactionWrapper(DbTransaction transaction)
6: {
7: this.DbTransaction = transaction;
8: }
9: public DbTransaction DbTransaction { get; private set; }
10: public bool IsRollBack { get; set; }
11: public void Rollback()
12: {
13: if (!this.IsRollBack)
14: {
15: this.DbTransaction.Rollback();
16: }
17: }
18: public void Commit()
19: {
20: this.DbTransaction.Commit();
21: }
22: public void Dispose()
23: {
24: this.DbTransaction.Dispose();
25: }
26: }
27: public abstract class Transaction : IDisposable
28: {
29: [ThreadStatic]
30: private static Transaction current;
31:
32: public bool Completed { get; private set; }
33: public DbTransactionWrapper DbTransactionWrapper { get; protected set; }
34: protected Transaction() { }
35: public void Rollback()
36: {
37: this.DbTransactionWrapper.Rollback();
38: }
39: public DependentTransaction DependentClone()
40: {
41: return new DependentTransaction(this);
42: }
43: public void Dispose()
44: {
45: this.DbTransactionWrapper.Dispose();
46: }
47: public static Transaction Current
48: {
49: get { return current; }
50: set { current = value; }
51: }
52: }
53: }
二、自定义CommittableTransaction和DependentTransaction
和定义在System.Transactions的事务一样,具体事务分为CommittableTransaction和DependentTransaction,前者可以使一个可以直接提交的独立事务,后者则是依附于某个事务的依赖事务。如下面的代码片断所示,我们直接基于某个DbTransaction来创建CommittableTransaction对象。而用于提交事务的Commit方法则直接调用DbTransaction的同名方法。
代码语言:js复制 1: public class CommittableTransaction : Transaction
2: {
3: public CommittableTransaction(DbTransaction dbTransaction)
4: {
5: this.DbTransaction = dbTransaction;
6: }
7: public void Commit()
8: {
9: this.DbTransaction.Commit();
10: }
11: }
下面是DependentTransaction的定义,我们直接基于某个作为内部事务的Transaction对象来创建DependentTransaction,而这个内部事务可以是一个CommittableTransaction,也可以是DependentTransaction。由于DependentTransaction只具有一个内部构造函数,所以DependentTransaction通过调用定义在Transaction的DependentClone方法来创建。
代码语言:js复制 1: public class DependentTransaction : Transaction
2: {
3: public Transaction InnerTransaction { get; private set; }
4: internal DependentTransaction(Transaction innerTransaction)
5: {
6: this.InnerTransaction = innerTransaction;
7: this.DbTransaction = this.InnerTransaction.DbTransaction;
8: }
9: }
三、自定义TransactionScope
我们在进行事务编程的时候只会使用到具有如下定义的TransactionScope类型。我们通过指定连接字符串名称、隔离级别以及用于创建DbProviderFactory的委托创建TransactionScope对象。在TransactionScope的构造函数中,如果通过Artech.Transactions.Transaction.Current属性表示的当前事务不存在,则根据DbProviderFactory创建DbConnection并调用BeginTransaction方法开启事务,并用被开启的DbTransaction创建CommittableTransaction对象。最终将创建的CommittableTransaction作为当前事务;服务过当前事务已经存在,则直接调用它的DependentClone方法创建的DependentTransaction作为当前事务。
代码语言:js复制 1: namespace Artech.Transactions
2: {
3: public class TransactionScope: IDisposable
4: {
5: private Transaction transaction = Transaction.Current;
6: public bool Completed { get; private set; }
7:
8: public TransactionScope(string connectionStringName, IsolationLevel isolationLevel = IsolationLevel.Unspecified,
9: Func<string, DbProviderFactory> getFactory = null)
10: {
11: if (null == transaction)
12: {
13: if (null == getFactory)
14: {
15: getFactory = cnnstringName => DbHelper.GetFactory(cnnstringName);
16: }
17: DbProviderFactory factory = getFactory(connectionStringName);
18: DbConnection connection = factory.CreateConnection();
19: connection.ConnectionString = ConfigurationManager.ConnectionStrings[connectionStringName].ConnectionString;
20: connection.Open();
21: DbTransaction dbTransaction = connection.BeginTransaction(isolationLevel);
22: Transaction.Current = new CommittableTransaction(dbTransaction);
23: }
24: else
25: {
26: Transaction.Current = transaction.DependentClone();
27: }
28: }
29:
30: public void Complete()
31: {
32: this.Completed = true;
33: }
34: public void Dispose()
35: {
36: Transaction current = Transaction.Current;
37: Transaction.Current = transaction;
38: if (!this.Completed)
39: {
40: current.Rollback();
41: }
42: CommittableTransaction committableTransaction = current as CommittableTransaction;
43: if (null != committableTransaction)
44: {
45: if (this.Completed)
46: {
47: committableTransaction.Commit();
48: }
49: committableTransaction.Dispose();
50: }
51: }
52: }
53: }
在事务操作完成之后必须调用Complete方法“提交事务”,此时我们将TransactionScope的Completed 属性设置为True。TransactionScope实现了IDisposable方法,Dispose方法具有这样的逻辑:先将当前状态还原成创建TransactionScope之初的状态,在还原之前先将当前事务保存下来。如果Completed属性为False,则直接调用Transaction的Rollback方法对事务进行回滚。如果Completed状态为True,而且当前事务为CommittableTransaction 则直接提交事务。
四、一个具有事务感知的DbHelper
为了演示通过捕捉当前事务来来控制具体事务的执行方式,我们写了如下一个DbHelper,其中ExecuteNonQuery用于在事务中执行指定的一段SQL。具体事务控制的逻辑是这样的:如果Artech.Transactions.Transaction.Current属性返回的事务存在,则将当前操作纳入封装的DbTransaction;如果System.Transactions.Transaction.Current属性返回的事务存在,操作的执行会自动纳入该事务中;如果上述两中环境事务均不存在,则创建一个单独的DbTransaction并将相应的操作纳入其中。
代码语言:js复制 1: public class DbHelper
2: {
3: //其他成员
4: public int ExecuteNonQuery(string commandText, IDictionary<string, object> parameters)
5: {
6: DbConnection connection = null;
7: DbCommand command = this.DbProviderFactory.CreateCommand();
8: DbTransaction dbTransaction = null;
9: try
10: {
11: command.CommandText = commandText;
12: parameters = parameters ?? new Dictionary<string, object>();
13: foreach (var item in parameters)
14: {
15: command.Parameters.Add(this.BuildDbParameter(item.Key, item.Value));
16: }
17: if (null != Artech.Transactions.Transaction.Current)
18: {
19: command.Connection = Artech.Transactions.Transaction.Current.DbTransactionWrapper.DbTransaction.Connection;
20: command.Transaction = Artech.Transactions.Transaction.Current.DbTransactionWrapper.DbTransaction;
21: }
22: else
23: {
24: connection = this.DbProviderFactory.CreateConnection();
25: connection.ConnectionString = this.ConnectionString;
26: command.Connection = connection;
27: connection.Open();
28: if (System.Transactions.Transaction.Current == null)
29: {
30: dbTransaction = connection.BeginTransaction();
31: command.Transaction = dbTransaction;
32: }
33: }
34: int result = command.ExecuteNonQuery();
35: if (null != dbTransaction)
36: {
37: dbTransaction.Commit();
38: }
39: return result;
40: }
41: catch
42: {
43: if (null != dbTransaction)
44: {
45: dbTransaction.Rollback();
46: }
47: throw;
48: }
49: finally
50: {
51: if (null != connection)
52: {
53: connection.Dispose();
54: }
55: if (null != dbTransaction)
56: {
57: dbTransaction.Dispose();
58: }
59: command.Dispose();
60: }
61: }
62: }
五、三种事务控制的性能
我们现在来测试批量操作在System.Transactions.TransactionScope、Artech.Transactions.TransactionScope和针对单独操作的DbTransaction的性能。我们在目标数据库中创建一个只包含Id和Name两个字段的数据表Users,并通过如下的CreateUser为该表添加一笔记录。
代码语言:js复制 1: static void CreateUser(string id, string name)
2: {
3: string sql = "INSERT Users(ID, Name) Values(@id, @name)";
4: Dictionary<string, object> parameters = new Dictionary<string, object>();
5: parameters.Add("id", id);
6: parameters.Add("name", name);
7: DbHelper.ExecuteNonQuery(sql, parameters);
8: }
而如下三个方法分别通过上述三种事务方式执行上面的这个方法,其中参数count为添加的数据量。
代码语言:js复制 1: private static void AddUsers1(int count)
2: {
3: using (System.Transactions.TransactionScope transactionScope = new System.Transactions.TransactionScope())
4: {
5: for (int i = 0; i < count; i )
6: {
7: CreateUser(Guid.NewGuid().ToString(), Guid.NewGuid().ToString());
8: }
9: transactionScope.Complete();
10: }
11: }
12:
13: private static void AddUsers2(int count)
14: {
15: using (Artech.Transactions.TransactionScope transactionScope = new Artech.Transactions.TransactionScope("TestDb"))
16: {
17: for (int i = 0; i < count; i )
18: {
19: CreateUser(Guid.NewGuid().ToString(), Guid.NewGuid().ToString());
20: }
21: transactionScope.Complete();
22: }
23: }
24:
25: private static void AddUsers3(int count)
26: {
27: for (int i = 0; i < count; i )
28: {
29: CreateUser(Guid.NewGuid().ToString(), Guid.NewGuid().ToString());
30: }
31: }
为了剔除由于数据表现有数据量对数据访问性能的影响,我们在每次进行数据批量插入之前都回调用具有如下定义的DeleteAllUsers方法将表中的数据删除殆尽。
代码语言:js复制 1: private static void DeleteAllUsers()
2: {
3: string sql = "DELETE USERS";
4: DbHelper.ExecuteNonQuery(sql, null);
5: }
下面是我们最终的测试程序,我们分别测试了数据量为100、1,000、10,000和100,000的数据写入操作分别在上述三种事务控制方式的耗时。
代码语言:js复制
1: Stopwatch stopWatch = new Stopwatch();
2:
3: //100
4: Console.WriteLine("100");
5: DeleteAllUsers();
6: stopWatch.Restart();
7: AddUsers1(100);
8: Console.WriteLine("{0, -36}: {1}", "System.Transactions.TransactionScope", stopWatch.ElapsedMilliseconds);
9:
10: DeleteAllUsers();
11: stopWatch.Restart();
12: AddUsers2(100);
13: Console.WriteLine("{0, -36}: {1}", "Artech.Transactions.TransactionScope", stopWatch.ElapsedMilliseconds);
14:
15: DeleteAllUsers();
16: stopWatch.Restart();
17: AddUsers3(100);
18: Console.WriteLine("{0, -36}: {1}n", "Single Transnaction", stopWatch.ElapsedMilliseconds);
19:
20: //1000
21: Console.WriteLine("1000");
22: DeleteAllUsers();
23: stopWatch.Restart();
24: AddUsers1(1000);
25: Console.WriteLine("{0, -36}: {1}", "System.Transactions.TransactionScope", stopWatch.ElapsedMilliseconds);
26:
27: DeleteAllUsers();
28: stopWatch.Restart();
29: AddUsers2(1000);
30: Console.WriteLine("{0, -36}: {1}", "Artech.Transactions.TransactionScope", stopWatch.ElapsedMilliseconds);
31:
32: DeleteAllUsers();
33: stopWatch.Restart();
34: AddUsers3(1000);
35: Console.WriteLine("{0, -36}: {1}n", "Single Transnaction", stopWatch.ElapsedMilliseconds);
36:
37: //10000
38: Console.WriteLine("10000");
39: DeleteAllUsers();
40: stopWatch.Restart();
41: AddUsers1(10000);
42: Console.WriteLine("{0, -36}: {1}", "System.Transactions.TransactionScope", stopWatch.ElapsedMilliseconds);
43:
44: DeleteAllUsers();
45: stopWatch.Restart();
46: AddUsers2(10000);
47: Console.WriteLine("{0, -36}: {1}", "Artech.Transactions.TransactionScope", stopWatch.ElapsedMilliseconds);
48:
49: DeleteAllUsers();
50: stopWatch.Restart();
51: AddUsers3(10000);
52: Console.WriteLine("{0, -36}: {1}n", "Single Transnaction", stopWatch.ElapsedMilliseconds);
53:
54: //100000
55: Console.WriteLine("100000");
56: DeleteAllUsers();
57: stopWatch.Restart();
58: AddUsers1(100000);
59: Console.WriteLine("{0, -36}: {1}", "System.Transactions.TransactionScope", stopWatch.ElapsedMilliseconds);
60:
61: DeleteAllUsers();
62: stopWatch.Restart();
63: AddUsers2(100000);
64: Console.WriteLine("{0, -36}: {1}", "Artech.Transactions.TransactionScope", stopWatch.ElapsedMilliseconds);
65:
66: DeleteAllUsers();
67: stopWatch.Restart();
68: AddUsers3(100000);
69: Console.WriteLine("{0, -36}: {1}", "Single Transaction", stopWatch.ElapsedMilliseconds);
下面是测试程序输出结果:
代码语言:js复制 1: 100
2: System.Transactions.TransactionScope: 28
3: Artech.Transactions.TransactionScope: 11
4: Single Transnaction : 57
5:
6: 1000
7: System.Transactions.TransactionScope: 140
8: Artech.Transactions.TransactionScope: 83
9: Single Transnaction : 469
10:
11: 10000
12: System.Transactions.TransactionScope: 1530
13: Artech.Transactions.TransactionScope: 826
14: Single Transnaction : 4659
15:
16: 100000
17: System.Transactions.TransactionScope: 13562
18: Artech.Transactions.TransactionScope: 8346
19: Single Transaction : 48277
上面的输出结果表明我们自定义的TransactionScope具有最好的性能,不过优势还不是很明显,这是由于连接池机制的原因。现在我们通过如下的方式将连接字符创对连接池的支持关闭:
代码语言:js复制 1: <configuration>
2: <connectionStrings>
3: <add name="TestDb"
4: providerName="System.Data.SqlClient"
5: connectionString="Data Source=.;Initial Catalog=TestDb;Integrated Security=True; Pooling=False;"/>
6: </connectionStrings>
7: </configuration>
再次运行我们的程序我们就会看到三种事务处理方式在性能上的显著差异(当数据量为100,000时,采用System.Transactions.TransactionScope直接就崩溃了)
代码语言:js复制 1: 100
2: System.Transactions.TransactionScope: 2318
3: Artech.Transactions.TransactionScope: 13
4: Single Transnaction : 310
5:
6: 1000
7: System.Transactions.TransactionScope: 2949
8: Artech.Transactions.TransactionScope: 124
9: Single Transnaction : 3623
10:
11: 10000
12: System.Transactions.TransactionScope: 32754
13: Artech.Transactions.TransactionScope: 1213
14: Single Transnaction : 30631
15:
16: 100000
17:
18: 未经处理的异常: System.Data.SqlClient.SqlException: Import of Microsoft Distributed Transaction Coordinator (MS DTC) transaction failed:...