做项目或系统设计时,依需求的不同,适必有不同的解决方案,有的以性能为主,有的以可扩展性为主,有的为了日后易于维护而做大量的组件化。本帖依此提供三种不同特性的「事务」ASP.NET
示例下载,包括:用一个数据库 Connection 即可高性能跨数据库写入、透过组件的函数调用即可参与事务、异步
(Asynchronous) 执行事务。
三个 ASP.NET 示例,其「事務」特性分別為:
(1) 兼顾性能与功能 - 利用 SqlConnection 类的 ChangeDatabase 方法,在单一个
Connection 中,跨越本机的两个数据库做 LTE (轻量级) 事务。
(2) 追求良好的架构、组件化及可维护性 - 利用 TransactionScope 类 + MS
DTC,直接经由各组件之间的函数调用,将其纳入同一个事务,亦可升级为 OleTx 分布式事务。
(3) 重视回应速度与用户体验 - 利用 CommittableTransaction + AsyncCallback
类,进行明确式的「异步 (Asynchronous)」事务。
-------------------------------------------------
本帖的示例下载点:
http://files.cnblogs.com/WizardWu/100204.zip
(执行第一個示例,需要 SQL Server 的 Northwind、AdventureWorksDW
数据库,不需要 DTC)
(执行第二個示例,需要 SQL Server 的 Northwind 数据库,并事先设置好 Windows
上的 DTC 分布式事务处理协调器)
(执行第三個示例,需要 SQL Server 的 Northwind 数据库,不需要 DTC)
---------------------------------------------------
(一)
示例一:兼顾性能与功能
有时我们只是临时需要在某一台机器上的 SQL Server,跨越其中的两个数据库做事务处理,或是其他一些简易的本机事务处理,此时只要透过一些
ADO.NET 的小技巧,利用同一个 Connection 对象,和最传统的 SqlTransaction
即可办到。如下方代码,透过 SqlConnection 的 ChangeDatabase 方法,即可在
Northwind、AdventureWorksDW 两个数据库之间切换,无须大费周章地升级为分布式事务,或浪费资源创建两次数据库的
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("跨越两个数据库的
LTE 本机事务成功 !");
}
catch (SqlException
ex)
{
tx.Rollback();
Response.Write("发生错误: "
+ ex.Message);
}
finally
{
cn.Close();
cn.Dispose();
}
}
市面上有好几本专讲 ADO.NET 的中、英文书籍,内容都相当不错,只可惜这方面的议题较少受到重视。
----------------------------------------------------------------------------
(二) 示例二:追求良好的架构、组件化及可维护性
有些写 Java 或比较重视架构设计的工程师,常会将一些特定的功能或商业逻辑,各自封装在多个组件或类之中
(Java 中的 Bean 或 SessionBean)。微软方面,自从 .NET 2.0 问世、TransactonScope
类和新世代的事务管理机制出现后,以往用 COM+ 的写法才能达到的功能,现在用 TransactonScope
类竟然很轻松地就能达成,这让 OOA/OOD、面向对象和 Design Patterns 的爱好者,在
.NET 平台上有了很好的解套方式。亦即可让对象的行为,在架构设计上能够独立,但却能随时决定是否要参与某个事务,或动态地决定是否要从
Local 事务升级成分布式事务。
例如下方的代码,为两个类 (或组件) 里各自的函数,他们可能是 ERP 中的「订单产生」组件,要调用「仓库对象」组件,去扣除一些库存量。透过「巢状
(nested);嵌套」的二或多个 TransactonScope 类,以及函数的直接调用,即可将对方纳入此一事务,并可自定义是否要纳入成为同一个事务,并且升级成分布式事务、启动
DTC,抑或拆分成两个事务、不启动 DTC。且不论是哪种选项,都能达到任一方抛出 Exception 时,双方都能自动
Rollback。
Class1
{
private
void
func1()
{
using
(TransactionScope scope =
new
TransactionScope())
{
Class2 c2 = new
Class2();
c2.func2();
//调用另一个组件的函数,直接将它纳入事务
scope.Complete();
}
}
}
Class2
{
public
void
func2()
{
using
(TransactionScope scope =
new
TransactionScope())
{
scope.Complete();
}
}
}
下图 1 为本帖下载示例 - 示例二的执行画面。如前述,我们用两个 Class 中函数调用的做法,但
Class 1、Class 2 的 TransactionScope,其 TransactionScopeOption
都设置为 Required (若已有现存的事务,则参与该个事务),表示双方要加入「同一个」事务中。因此
Class 1 所插入数据库的一条记录,Class 2 立即可 SELECT 得到它,因为他们是在「同一个」事务中。但代价是会启动
MS DTC、自动升级成 OleTx 分布式事务。虽然这两个 Class 是在同一台机器中,但因为在同一个事务中,开启了两条数据库的
Connection,因此仍会自动从本机的轻量级 LTM 事务管理员,升级成 OleTx 事务管理员 (依赖
RPC 远端程序调用),也因此会自动启用 MS DTC (若 DTC 已设置好)。
但若您把 Class 2 的 TransactionScope,其 TransactionScopeOption
设置为 RequiresNew (不管是否有现存事务,都一律创建新的事务),您会发现 MS DTC 不会启动了,因为他们已被拆分成「二个事务」,也因此
Class 1 所插入数据库的一条记录,Class 2 已无法立即 SELECT 取得,因为他们不在「同一个」事务中。
但不论是前述哪种做法,仍都能达到任一方引发 Exception 时,双方都能自动 Rollback。若您以前,曾经梦想透过
Web Service 彼此的调用,来达到事务的完整性,会发现情形如同前述的第二种,亦即被拆分成「二个事务」,虽然任一方抛出
Exception 时,双方都能自动 Rollback,但由于是拆分成二个事务,因此第一个 Web Service
所插入数据库的一条记录,第二个 Web Service 无法立即取得。而这点,就某些系统的设计需求上,虽然看似小瑕疵,却是不被允许的。可能有些人宁愿用第一种做法,包成「同一个」事务,宁可启动
MS DTC,牺牲一些性能,也要达成事务的高度完整性。
图 1 示例二的执行画面
示例二的
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 出数据库最新插入的这一笔记录,其 id 值 (identity, 由数据库自动增号)
string
strSql = "INSERT
INTO Employees (LastName, FirstName) VALUES('Wu',
'Wizard') ; SELECT @@identity; ";
//Required 选项:
当前环境若无事务,则创建新事务,否则就加入当前环境的同一个事务
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());
//调用 Class 2
的函数,将其也加入同一个事务
Class2 c2 =
new
Class2();
strReturn = c2.func2(intTheNewestID);
scope.Complete();
}
}
catch
(Exception ex)
{
throw
new
Exception("组件一 -
发生数据库访问错误: " +
ex.ToString());
}
finally
{
if
(cmd != null)
cmd.Dispose();
if
(conn.State == ConnectionState.Open)
{
conn.Close();
}
conn.Dispose();
}
}
return
strReturn;
//返回前台的网页中显示
}
}
示例二的
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 选项:
当前环境若无事务,则创建新事务,否则就加入当前环境的同一个事务。在此例中,会启动 DTC,第二句
Select 会成功。
//RequiresNew 选项: 总是创建新的事务,会造成 Class1、Class2
不会处于同一个事务里。在此例中,不会启动 DTC,第二句 Select 会失败。
//Suppress 选项: 不加入此一事务。
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();
//取得组件一里,刚刚才插入的那一笔记录,以确认组件一、组件二确实是在同一个事务中执行,而不是拆分成两个事务
cmd2 =
new
SqlCommand(strSql2, conn);
strReturn = cmd2.ExecuteScalar().ToString();
scope.Complete();
}
}
catch
(Exception ex)
{
throw
new
Exception("组件二 -
数据库访问发生错误: " +
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 的设置。如下图 2,最左侧为没有事务的代码,当其调用了 scope1
时 (Required),创建了全新的事务 Transaction A。接下来,当创建了第二个 scope2,或如本帖示例二调用了第二个组件时,由于也是
Reuqired,因此和本帖示例二的情况一模一样,双方会包在「同一个」事务 A 中,并可能会启动 MS
DTC。
当创建了第三个 scope3,或呼叫了第三个组件时,由于是 ReuqiresNew,因此会创建「另一个」事务
Transaction B。而当创建了第四个 scope4,或调用了第四个组件时,因设置为 Suppress
(表示无论如何不加入事务),因此其会独立执行,不参与任何事务。此种 Supppress 设置,适用于调用第三方厂商或协力厂商的组件,或是单纯执行
SELECT 语句,不需要或不想加入事务时的情形。
//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 设置的执行结果
在我先前写过的文章「网站性能优化 - 数据库及服务器架构篇」,里面的图 3 -「物理」上的分层,各种商业逻辑可能存在多台物理主机上,里面有提到,这些不同功能的组件或商业逻辑,可能在同一台
AP Server 上,也可能分布在不同的服务器上。因此要以哪种方式来调用,或同一台机器上的组件,是否有必要牺牲一些性能、启用
DTC 来运作,以达成特定需求的系统设计,应事先做好评估。
----------------------------------------------------------------------------
(三) 示例三:重视回应速度与用户体验
若事务访问了多个数据库,或因网络太慢,让事务时间拉太长,我们还可考虑用 CommittableTransaction
类,以「异步 (Asynchronous)」方式来处理事务。其原理为利用另一条背景线程,来等待事务处理的结果,让主程序
(客户端的浏览器) 能先进行其他的操作,避免让用户处于等待的情况。
如下方示例三的部分代码,执行异步事务时,需提供一个 Callback 方法,在 Commit 时自动调用,亦即下方示例的
OnCommitted 方法。当执行到这个方法时,便会从 Thread Pool 里取得一条线程,进行异步的事务确认。
示例三
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 进行明确式事务
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
函数为 OnCommitted
AsyncCallback ac
= new
AsyncCallback(OnCommitted);
tran.BeginCommit(ac,
null);
//开始一个异步事务
//tran.Commit(); //同步事务的写法
}
}
catch
(Exception ex)
{
tran.Rollback();
Response.Write("程序发生错误:
" + ex.Message);
}
finally
{
if
(cmd != null)
cmd.Dispose();
if
(conn.State == ConnectionState.Open)
{
conn.Close();
}
conn.Dispose();
}
}
}
//执行到这个方法时,会从
Thread Pool 里取得一条线程,进行异步的事务
private
void
OnCommitted(IAsyncResult ar)
//传入一个 IAsyncResult 参数
{
CommittableTransaction Tx;
Tx = (CommittableTransaction)ar;
try
{
using
((Tx))
{
Tx.EndCommit(ar);
//结束异步事务
}
Response.Write("异步事务完成,已成功插入一条记录。");
}
catch
(TransactionException ex)
{
Tx.Rollback();
Response.Write("异步事务失败,错误信息为:"
+ ex.Message);
}
finally
{
if
(Tx != null)
Tx.Dispose();
}
}
}
----------------------------------------------------------------------------
本帖第一、第三个示例,执行时并不会启动 MS DTC;而第二个示例,则要看 TransactionScopeOption
的设置情形,依本帖下载示例的缺省值,由于双方都为 Required,因此默认会启动 DTC;但若您将示例中
Class 2 里 func 2 改为 RequiresNew,则不会启动 DTC。因此实务上,一个系统该如何去设计,是否要为了彻底的组件化、易于日后维护和扩展,而牺牲一些事务处理上的性能
(写 Java/J2EE 的人好像常干这种事),应视系统和项目的需求,而非永远以一套固定的设计方式或代码写法,就想套用在所有的项目中。
图 3 MS DTC 统计画面
----------------------------------------------------------------------------
相关文章:
[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.cnblogs.com/perhaps/archive/2005/08/17/216863.html
[3] SQL Server 的 System.Transactions 集成 (ADO.NET)
http://msdn.microsoft.com/zh-cn/library/ms172070.aspx
[4] 谈谈分布式事务(Distributed Transaction)[共5篇] - Artech
- 博客园
http://www.cnblogs.com/artech/archive/2010/01/31/1660433.html
[5] WCF系列_分布式事务
http://www.cnblogs.com/chnking/archive/2010/01/10/1643362.html
http://www.cnblogs.com/chnking/archive/2010/01/10/1643384.html
[6] 网站性能优化 - 数据库及服务器架构篇
http://www.cnblogs.com/WizardWu/archive/2009/09/22/1571499.html
----------------------------------------------------------------------------
|