做項目或系統(tǒng)設(shè)計時,依需求的不同,適必有不同的解決方案,有的以性能為主,有的以可擴展性為主,有的為了日后易于維護而做大量的組件化。本帖依此提供三種不同特性的「事務(wù)」ASP.NET 示例下載,包括:用一個數(shù)據(jù)庫 Connection 即可高性能跨數(shù)據(jù)庫寫入、透過組件的函數(shù)調(diào)用即可參與事務(wù)、異步 (Asynchronous) 執(zhí)行事務(wù)。
三個 ASP.NET 示例,其「事務(wù)」特性分別為:
(1) 兼顧性能與功能 - 利用 SqlConnection 類的 ChangeDatabase 方法,在單一個 Connection 中,跨越本機的兩個數(shù)據(jù)庫做 LTE (輕量級) 事務(wù)。
(2) 追求良好的架構(gòu)、組件化及可維護性 - 利用 TransactionScope 類 + MS DTC,直接經(jīng)由各組件之間的函數(shù)調(diào)用,將其納入同一個事務(wù),亦可升級為 OleTx 分布式事務(wù)。
(3) 重視回應(yīng)速度與用戶體驗 - 利用 CommittableTransaction + AsyncCallback 類,進行明確式的「異步 (Asynchronous)」事務(wù)。
-------------------------------------------------
本帖的示例下載點:
https://files.cnblogs.com/WizardWu/100204.zip
(執(zhí)行第一個示例,需要 SQL Server 的 Northwind、AdventureWorksDW 數(shù)據(jù)庫,不需要 DTC)
(執(zhí)行第二個示例,需要 SQL Server 的 Northwind 數(shù)據(jù)庫,并事先設(shè)置好 Windows 上的 DTC 分布式事務(wù)處理協(xié)調(diào)器)
(執(zhí)行第三個示例,需要 SQL Server 的 Northwind 數(shù)據(jù)庫,不需要 DTC)
---------------------------------------------------
(一) 示例一:兼顧性能與功能
有時我們只是臨時需要在某一臺機器上的 SQL Server,跨越其中的兩個數(shù)據(jù)庫做事務(wù)處理,或是其他一些簡易的本機事務(wù)處理,此時只要透過一些 ADO.NET 的小技巧,利用同一個 Connection 對象,和最傳統(tǒng)的 SqlTransaction 即可辦到。如下方代碼,透過 SqlConnection 的 ChangeDatabase 方法,即可在 Northwind、AdventureWorksDW 兩個數(shù)據(jù)庫之間切換,無須大費周章地升級為分布式事務(wù),或浪費資源創(chuàng)建兩次數(shù)據(jù)庫的 Connection。
示例一
protected void Button1_Click(object sender, EventArgs e)
{
SqlConnection cn = new SqlConnection("server=localhost;database=Northwind;integrated security=true");
SqlTransaction tx = null;
try
{
cn.Open();
tx = cn.BeginTransaction();
SqlCommand cmd1 = new SqlCommand("INSERT INTO Employees (LastName, FirstName) VALUES('Wu', 'Wizard')", cn);
cmd1.Transaction = tx;
cmd1.ExecuteNonQuery();
cn.ChangeDatabase("AdventureWorksDW");
SqlCommand cmd2 = new SqlCommand("INSERT INTO DimGeography (City) VALUES ('Taipei')", cn);
cmd2.Transaction = tx;
cmd2.ExecuteNonQuery();
tx.Commit();
Response.Write("跨越兩個數(shù)據(jù)庫的 LTE 本機事務(wù)成功 !");
}
catch (SqlException ex)
{
tx.Rollback();
Response.Write("發(fā)生錯誤: " + ex.Message);
}
finally
{
cn.Close();
cn.Dispose();
}
}
市面上有好幾本專講 ADO.NET 的中、英文書籍,內(nèi)容都相當不錯,只可惜這方面的議題較少受到重視。
----------------------------------------------------------------------------
(二) 示例二:追求良好的架構(gòu)、組件化及可維護性
有些寫 Java 或比較重視架構(gòu)設(shè)計的工程師,常會將一些特定的功能或商業(yè)邏輯,各自封裝在多個組件或類之中 (Java 中的 Bean 或 SessionBean)。微軟方面,自從 .NET 2.0 問世、TransactonScope 類和新世代的事務(wù)管理機制出現(xiàn)后,以往用 COM+ 的寫法才能達到的功能,現(xiàn)在用 TransactonScope 類竟然很輕松地就能達成,這讓 OOA/OOD、面向?qū)ο蠛?Design Patterns 的愛好者,在 .NET 平臺上有了很好的解套方式。亦即可讓對象的行為,在架構(gòu)設(shè)計上能夠獨立,但卻能隨時決定是否要參與某個事務(wù),或動態(tài)地決定是否要從 Local 事務(wù)升級成分布式事務(wù)。
例如下方的代碼,為兩個類 (或組件) 里各自的函數(shù),他們可能是 ERP 中的「訂單產(chǎn)生」組件,要調(diào)用「倉庫對象」組件,去扣除一些庫存量。透過「巢狀 (nested);嵌套」的二或多個 TransactonScope 類,以及函數(shù)的直接調(diào)用,即可將對方納入此一事務(wù),并可自定義是否要納入成為同一個事務(wù),并且升級成分布式事務(wù)、啟動 DTC,抑或拆分成兩個事務(wù)、不啟動 DTC。且不論是哪種選項,都能達到任一方拋出 Exception 時,雙方都能自動 Rollback。
Class1
{
private void func1()
{
using (TransactionScope scope = new TransactionScope())
{
Class2 c2 = new Class2();
c2.func2(); //調(diào)用另一個組件的函數(shù),直接將它納入事務(wù)
scope.Complete();
}
}
}
Class2
{
public void func2()
{
using (TransactionScope scope = new TransactionScope())
{
scope.Complete();
}
}
}
下圖 1 為本帖下載示例 - 示例二的執(zhí)行畫面。如前述,我們用兩個 Class 中函數(shù)調(diào)用的做法,但 Class 1、Class 2 的 TransactionScope,其 TransactionScopeOption 都設(shè)置為 Required (若已有現(xiàn)存的事務(wù),則參與該個事務(wù)),表示雙方要加入「同一個」事務(wù)中。因此 Class 1 所插入數(shù)據(jù)庫的一條記錄,Class 2 立即可 SELECT 得到它,因為他們是在「同一個」事務(wù)中。但代價是會啟動 MS DTC、自動升級成 OleTx 分布式事務(wù)。雖然這兩個 Class 是在同一臺機器中,但因為在同一個事務(wù)中,開啟了兩條數(shù)據(jù)庫的 Connection,因此仍會自動從本機的輕量級 LTM 事務(wù)管理員,升級成 OleTx 事務(wù)管理員 (依賴 RPC 遠端程序調(diào)用),也因此會自動啟用 MS DTC (若 DTC 已設(shè)置好)。
但若您把 Class 2 的 TransactionScope,其 TransactionScopeOption 設(shè)置為 RequiresNew (不管是否有現(xiàn)存事務(wù),都一律創(chuàng)建新的事務(wù)),您會發(fā)現(xiàn) MS DTC 不會啟動了,因為他們已被拆分成「二個事務(wù)」,也因此 Class 1 所插入數(shù)據(jù)庫的一條記錄,Class 2 已無法立即 SELECT 取得,因為他們不在「同一個」事務(wù)中。
但不論是前述哪種做法,仍都能達到任一方引發(fā) Exception 時,雙方都能自動 Rollback。若您以前,曾經(jīng)夢想透過 Web Service 彼此的調(diào)用,來達到事務(wù)的完整性,會發(fā)現(xiàn)情形如同前述的第二種,亦即被拆分成「二個事務(wù)」,雖然任一方拋出 Exception 時,雙方都能自動 Rollback,但由于是拆分成二個事務(wù),因此第一個 Web Service 所插入數(shù)據(jù)庫的一條記錄,第二個 Web Service 無法立即取得。而這點,就某些系統(tǒng)的設(shè)計需求上,雖然看似小瑕疵,卻是不被允許的。可能有些人寧愿用第一種做法,包成「同一個」事務(wù),寧可啟動 MS DTC,犧牲一些性能,也要達成事務(wù)的高度完整性。

圖 1 示例二的執(zhí)行畫面
示例二的 Class1 (組件一)
using System;
using System.Data;
using System.Transactions;
using System.Data.SqlClient;
public class Class1
{
private string strConnString = System.Configuration.ConfigurationManager.ConnectionStrings["Conn_Northwind"].ToString();
public Class1()
{
}
public string func1()
{
SqlConnection conn = null;
SqlCommand cmd = null;
int intTheNewestID = 0;
string strReturn = "";
//Insert 后,立即 Select 出數(shù)據(jù)庫最新插入的這一筆記錄,其 id 值 (identity, 由數(shù)據(jù)庫自動增號)
string strSql = "INSERT INTO Employees (LastName, FirstName) VALUES('Wu', 'Wizard') ; SELECT @@identity; ";
//Required 選項: 當前環(huán)境若無事務(wù),則創(chuàng)建新事務(wù),否則就加入當前環(huán)境的同一個事務(wù)
using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required))
{
try
{
conn = new SqlConnection(strConnString);
conn.Open();
if (conn.State == ConnectionState.Open)
{
cmd = new SqlCommand(strSql, conn);
intTheNewestID = Convert.ToInt32(cmd.ExecuteScalar());
//調(diào)用 Class 2 的函數(shù),將其也加入同一個事務(wù)
Class2 c2 = new Class2();
strReturn = c2.func2(intTheNewestID);
scope.Complete();
}
}
catch (Exception ex)
{
throw new Exception("組件一 - 發(fā)生數(shù)據(jù)庫訪問錯誤: " + ex.ToString());
}
finally
{
if (cmd != null)
cmd.Dispose();
if (conn.State == ConnectionState.Open)
{
conn.Close();
}
conn.Dispose();
}
}
return strReturn; //返回前臺的網(wǎng)頁中顯示
}
}
示例二的 Class2 (組件二)
using System;
using System.Data;
using System.Transactions;
using System.Data.SqlClient;
public class Class2
{
private string strConnString = System.Configuration.ConfigurationManager.ConnectionStrings["Conn_Northwind"].ToString();
public Class2()
{
}
public string func2(int intTheNewestID)
{
SqlConnection conn = null;
SqlCommand cmd1 = null;
SqlCommand cmd2 = null;
int intInserted = 0;
string strReturn = "";
string strSql1 = "INSERT INTO Employees (LastName, FirstName) VALUES('Lee', 'David')";
string strSql2 = "SELECT LastName FROM Employees WHERE EmployeeID=" + intTheNewestID;
//Required 選項: 當前環(huán)境若無事務(wù),則創(chuàng)建新事務(wù),否則就加入當前環(huán)境的同一個事務(wù)。在此例中,會啟動 DTC,第二句 Select 會成功。
//RequiresNew 選項: 總是創(chuàng)建新的事務(wù),會造成 Class1、Class2 不會處于同一個事務(wù)里。在此例中,不會啟動 DTC,第二句 Select 會失敗。
//Suppress 選項: 不加入此一事務(wù)。
using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required))
{
try
{
conn = new SqlConnection(strConnString);
conn.Open();
if (conn.State == ConnectionState.Open)
{
cmd1 = new SqlCommand(strSql1, conn);
intInserted = cmd1.ExecuteNonQuery();
//取得組件一里,剛剛才插入的那一筆記錄,以確認組件一、組件二確實是在同一個事務(wù)中執(zhí)行,而不是拆分成兩個事務(wù)
cmd2 = new SqlCommand(strSql2, conn);
strReturn = cmd2.ExecuteScalar().ToString();
scope.Complete();
}
}
catch (Exception ex)
{
throw new Exception("組件二 - 數(shù)據(jù)庫訪問發(fā)生錯誤: " + ex.ToString());
}
finally
{
if (cmd1 != null)
cmd1.Dispose();
if (cmd2 != null)
cmd2.Dispose();
if (conn.State == ConnectionState.Open)
{
conn.Close();
}
conn.Dispose();
}
}
return strReturn; //返回組件一
}
}
MSDN 上有一篇文章 [1],或一些 ADO.NET 書籍,有介紹此種 Nested TransactionScope,及其 TransactionScopeOption 的設(shè)置。如下圖 2,最左側(cè)為沒有事務(wù)的代碼,當其調(diào)用了 scope1 時 (Required),創(chuàng)建了全新的事務(wù) Transaction A。接下來,當創(chuàng)建了第二個 scope2,或如本帖示例二調(diào)用了第二個組件時,由于也是 Reuqired,因此和本帖示例二的情況一模一樣,雙方會包在「同一個」事務(wù) A 中,并可能會啟動 MS DTC。
當創(chuàng)建了第三個 scope3,或呼叫了第三個組件時,由于是 ReuqiresNew,因此會創(chuàng)建「另一個」事務(wù) Transaction B。而當創(chuàng)建了第四個 scope4,或調(diào)用了第四個組件時,因設(shè)置為 Suppress (表示無論如何不加入事務(wù)),因此其會獨立執(zhí)行,不參與任何事務(wù)。此種 Supppress 設(shè)置,適用于調(diào)用第三方廠商或協(xié)力廠商的組件,或是單純執(zhí)行 SELECT 語句,不需要或不想加入事務(wù)時的情形。
//Default TransactionScopeOption is "Required"
using(TransactionScope scope1 = new TransactionScope())
{
using(TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Required))
{...}
using(TransactionScope scope3 = new TransactionScope(TransactionScopeOption.RequiresNew))
{...}
using(TransactionScope scope4 = new TransactionScope(TransactionScopeOption.Suppress))
{...}
//...
}

圖 2 不同 TransactionScopeOption 設(shè)置的執(zhí)行結(jié)果
在我先前寫過的文章「網(wǎng)站性能優(yōu)化 - 數(shù)據(jù)庫及服務(wù)器架構(gòu)篇」,里面的圖 3 -「物理」上的分層,各種商業(yè)邏輯可能存在多臺物理主機上,里面有提到,這些不同功能的組件或商業(yè)邏輯,可能在同一臺 AP Server 上,也可能分布在不同的服務(wù)器上。因此要以哪種方式來調(diào)用,或同一臺機器上的組件,是否有必要犧牲一些性能、啟用 DTC 來運作,以達成特定需求的系統(tǒng)設(shè)計,應(yīng)事先做好評估。
----------------------------------------------------------------------------
(三) 示例三:重視回應(yīng)速度與用戶體驗
若事務(wù)訪問了多個數(shù)據(jù)庫,或因網(wǎng)絡(luò)太慢,讓事務(wù)時間拉太長,我們還可考慮用 CommittableTransaction 類,以「異步 (Asynchronous)」方式來處理事務(wù)。其原理為利用另一條背景線程,來等待事務(wù)處理的結(jié)果,讓主程序 (客戶端的瀏覽器) 能先進行其他的操作,避免讓用戶處于等待的情況。
如下方示例三的部分代碼,執(zhí)行異步事務(wù)時,需提供一個 Callback 方法,在 Commit 時自動調(diào)用,亦即下方示例的 OnCommitted 方法。當執(zhí)行到這個方法時,便會從 Thread Pool 里取得一條線程,進行異步的事務(wù)確認。
示例三
using System;
using System.Data;
using System.Transactions;
using System.Data.SqlClient;
public partial class _Default : System.Web.UI.Page
{
private string strConnString = System.Configuration.ConfigurationManager.ConnectionStrings["Conn_Northwind"].ToString();
protected void Page_Load(object sender, EventArgs e)
{
}
protected void Button1_Click(object sender, EventArgs e)
{
SqlConnection conn = null;
SqlCommand cmd = null;
string strSql = "INSERT INTO Employees (LastName, FirstName) VALUES('Wu', 'Wizard')";
//用 CommittableTransaction 進行明確式事務(wù)
using (CommittableTransaction tran = new CommittableTransaction())
{
try
{
conn = new SqlConnection(strConnString);
conn.Open();
conn.EnlistTransaction(tran);
if (conn.State == ConnectionState.Open)
{
cmd = new SqlCommand(strSql, conn);
cmd.ExecuteNonQuery();
//指定 Callback 函數(shù)為 OnCommitted
AsyncCallback ac = new AsyncCallback(OnCommitted);
tran.BeginCommit(ac, null); //開始一個異步事務(wù)
//tran.Commit(); //同步事務(wù)的寫法
}
}
catch (Exception ex)
{
tran.Rollback();
Response.Write("程序發(fā)生錯誤: " + ex.Message);
}
finally
{
if (cmd != null)
cmd.Dispose();
if (conn.State == ConnectionState.Open)
{
conn.Close();
}
conn.Dispose();
}
}
}
//執(zhí)行到這個方法時,會從 Thread Pool 里取得一條線程,進行異步的事務(wù)
private void OnCommitted(IAsyncResult ar) //傳入一個 IAsyncResult 參數(shù)
{
CommittableTransaction Tx;
Tx = (CommittableTransaction)ar;
try
{
using ((Tx))
{
Tx.EndCommit(ar); //結(jié)束異步事務(wù)
}
Response.Write("異步事務(wù)完成,已成功插入一條記錄。");
}
catch (TransactionException ex)
{
Tx.Rollback();
Response.Write("異步事務(wù)失敗,錯誤信息為:" + ex.Message);
}
finally
{
if (Tx != null)
Tx.Dispose();
}
}
}
----------------------------------------------------------------------------
本帖第一、第三個示例,執(zhí)行時并不會啟動 MS DTC;而第二個示例,則要看 TransactionScopeOption 的設(shè)置情形,依本帖下載示例的缺省值,由于雙方都為 Required,因此默認會啟動 DTC;但若您將示例中 Class 2 里 func 2 改為 RequiresNew,則不會啟動 DTC。因此實務(wù)上,一個系統(tǒng)該如何去設(shè)計,是否要為了徹底的組件化、易于日后維護和擴展,而犧牲一些事務(wù)處理上的性能 (寫 Java/J2EE 的人好像常干這種事),應(yīng)視系統(tǒng)和項目的需求,而非永遠以一套固定的設(shè)計方式或代碼寫法,就想套用在所有的項目中。

圖 3 MS DTC 統(tǒng)計畫面
----------------------------------------------------------------------------
相關(guān)文章:
[1] Introducing System.Transactions in the .NET Framework 2.0
http://msdn.microsoft.com/en-us/library/ms973865.aspx
[2] J2EE與.NET在Transaction Scope上的比較
http://www.rzrgm.cn/perhaps/archive/2005/08/17/216863.html
[3] SQL Server 的 System.Transactions 集成 (ADO.NET)
http://msdn.microsoft.com/zh-cn/library/ms172070.aspx
[4] 談?wù)劮植际绞聞?wù)(Distributed Transaction)[共5篇] - Artech - 博客園
http://www.rzrgm.cn/artech/archive/2010/01/31/1660433.html
[5] WCF系列_分布式事務(wù)
http://www.rzrgm.cn/chnking/archive/2010/01/10/1643362.html
http://www.rzrgm.cn/chnking/archive/2010/01/10/1643384.html
[6] 網(wǎng)站性能優(yōu)化 - 數(shù)據(jù)庫及服務(wù)器架構(gòu)篇
http://www.rzrgm.cn/WizardWu/archive/2009/09/22/1571499.html
----------------------------------------------------------------------------

浙公網(wǎng)安備 33010602011771號