閱讀376 返回首頁    go windows


像TransactionScope一樣使用DbTransaction

System.Transactions.TransactionScope為了提供一種非常方便的實現分布式事務的方式,但是在某些情況下為了阻止本地事務向分布式事務提升,我們隻能通過DbTransaction進行事務控製。在進行數據庫操作的時候,我們經常會遇到這樣的事務場景:當某個方法單獨執行的時候,它應該在一個單一的DbTransaction中執行;當多個方法一起執行的時候,它們應用共同在DbTransaction中執行。在本篇文章中我們通過對DbTransaction進行封裝,提供一種類似於TransactionScope的編程方式來解決這個問題。[源代碼從這裏下載]

目錄
一、自定義的Transaction
二、自定義CommittableTransaction和DependentTransaction
三、自定義TransactionScope
四、一個具有事務感知的DbHelper
五、三種事務控製的性能

我們完全采用System.Transactions的應用編程接口的設計定義基於DbTransaction的相關類型,首選來看看具有如下定義的表示事務的Transaction類型。Transaction是一個抽象類,具有DbTransactionWrapper和Completed兩個屬性,前者表示對DbTransaction的封裝,後者表示事務操作是否已經完成。靜態屬性Current表示當前事務,這是一個基於當前線程的靜態字段。Rollback和Dispose直接調用了DbTransactionWrapper的同名方法。

   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: }

和定義在System.Transactions的事務一樣,具體事務分為CommittableTransaction和DependentTransaction,前者可以使一個可以直接提交的獨立事務,後者則是依附於某個事務的依賴事務。如下麵的代碼片斷所示,我們直接基於某個DbTransaction來創建CommittableTransaction對象。而用於提交事務的Commit方法則直接調用DbTransaction的同名方法。

   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方法來創建。

   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類型。我們通過指定連接字符串名稱、隔離級別以及用於創建DbProviderFactory的委托創建TransactionScope對象。在TransactionScope的構造函數中,如果通過Artech.Transactions.Transaction.Current屬性表示的當前事務不存在,則根據DbProviderFactory創建DbConnection並調用BeginTransaction方法開啟事務,並用被開啟的DbTransaction創建CommittableTransaction對象。最終將創建的CommittableTransaction作為當前事務;服務過當前事務已經存在,則直接調用它的DependentClone方法創建的DependentTransaction作為當前事務。

   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,其中ExecuteNonQuery用於在事務中執行指定的一段SQL。具體事務控製的邏輯是這樣的:如果Artech.Transactions.Transaction.Current屬性返回的事務存在,則將當前操作納入封裝的DbTransaction;如果System.Transactions.Transaction.Current屬性返回的事務存在,操作的執行會自動納入該事務中;如果上述兩中環境事務均不存在,則創建一個單獨的DbTransaction並將相應的操作納入其中。

   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為該表添加一筆記錄。

   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為添加的數據量。

   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方法將表中的數據刪除殆盡。

   1: private static void DeleteAllUsers()
   2: {
   3:     string sql = "DELETE USERS";
   4:     DbHelper.ExecuteNonQuery(sql, null);
   5: }

下麵是我們最終的測試程序,我們分別測試了數據量為100、1,000、10,000和100,000的數據寫入操作分別在上述三種事務控製方式的耗時。

   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);

下麵是測試程序輸出結果:

   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具有最好的性能,不過優勢還不是很明顯,這是由於連接池機製的原因。現在我們通過如下的方式將連接字符創對連接池的支持關閉:

   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直接就崩潰了)

   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:...

作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-26 14:04:53

  上一篇:go  WCF 4.0一個鮮為人知的改變[兼書名征集]
  下一篇:go  網站托管:遇到問題友鏈怎麼辦?