求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
 
事务与系统架构设计
 

2010-10-12 作者:wizardwu 来源:wizardwu的blog

 

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

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



专家视角看IT与架构
软件架构设计
面向服务体系架构和业务组件
人人网移动开发架构
架构腐化之谜
谈平台即服务PaaS


面向应用的架构设计实践
单元测试+重构+设计模式
软件架构师—高级实践
软件架构设计方法、案例与实践
嵌入式软件架构设计—高级实践
SOA体系结构实践


锐安科技 软件架构设计方法
成都 嵌入式软件架构设计
上海汽车 嵌入式软件架构设计
北京 软件架构设计
上海 软件架构设计案例与实践
北京 架构设计方法案例与实践
深圳 架构设计方法案例与实践
嵌入式软件架构设计—高级实践
更多...