WizardWu 編程網

一位台灣的工程師,接觸 .NET 逾十年,近年研究 SQL Server、Performance Tuning、手機應用

博客园 首页 新随笔 联系 订阅 管理
  103 随笔 :: 0 文章 :: 1097 评论

做项目或系统设计时,依需求的不同,适必有不同的解决方案,有的以性能为主,有的以可扩展性为主,有的为了日后易于维护而做大量的组件化。本帖依此提供三种不同特性的「事务」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

 

----------------------------------------------------------------------------

 

posted on 2010-02-04 03:18  WizardWu  阅读(5319)  评论(8编辑  收藏