System.Transactions:实现你自己的Resource Manager
By Sahil Malik
.net 2.0所带来最大的变化之一也许就是System.Transactions命名空间的引入。在我以前关于SQLCLR的文章中,也简略的提到过这个命名空间。但是谁说事务的概念只能局限于数据库?难道我们在数据库以外的领域就不需要可靠的代码了么?答案是,我们当然需要! 这正是你不能忽视System.Transactions命名空间的原因,也是她将在下一代微软平台引起深刻变革的原因。让我们首先来看看 System.Transactions是如何工作的。
System.Transactions如何工作?
System.Transactions为您提供最简洁明了的方式来实现事务方式的各种操作。你当然可以用各种各样的方式使用她,但也许最典型的一个实现就是如下,将事务操作包裹在TransactionScope(用来构建代码段事务的一个实例变量)里面:
using (TransactionScope ts = new TransactionScope())
{
// 事务代码放这里
ts.Complete();
}
在上面注释的部分里面,你可以使用持有或实现了resource manager(RM)的类。
事务工作于各种被RM管理着的资源上。RM同其他实体(典型的包括:其他进程或者被称为transaction managers(TMs)的服务)协同工作。
RM与TM在事务中协同工作的方式常被称为”两段提交过程”。
下面是典型的“两段提交过程”工作流:
1.RM加入事务;
2.RM做完准备工作后,向TM发送第一阶段完成信号。这个阶段也被称为”准备阶段”;
3.TM在所有RMs成功执行完准备工作后,向所有RMs发送绿灯信号;
4.RMs获得绿灯信号后实际开始提交工作(如果收到红灯信号,则回滚他们自己的工作)。这是第二个阶段–“提交阶段”;
5.否则,事务协调器同所有RMs进行协调以保证他们要么成功,要么一起回滚;
上面提到的整个过程都由System.Transactions框架提供,例如,使用MSDTC(Microsoft分布式事务协调器)作为TMs。为了使用System.Transactions来管理参与事务操作的资源,你必须要么使用现有的RM或者实现你自己的RM。
就目前而论, System.Transactions已可以使用,但实际能应用的RMs却很少,所以很多程序员可能会发现实现自己的RM非常有用。在这之后,可以将这些RMs同未来.net框架中或第三方提供的TM协同工作。另外,同.net 2.0一同发布的一个很常用的RM就是SqlConnection类。本文中会以此类和一个自定义的RM为例,描述它们究竟如何同TM一起工作。但是,如果你确实不得不实现自己的RM,最重要的问题恐怕就是–“RM究竟要做些什么?”
RM究竟要做些什么?
影响实现你自己的RM的一个关键问题就是:这个RM管理的资源本身的特性是持久的还是易变的?
在典型的”两段提交过程”中,持久的资源需要”失败恢复”。一个很好的例子就是事务文件拷贝。如果你不得不实现一个RM来封装拷贝文件的操作到一个事务中,文件可以在第一个阶段被实际拷贝。如果RM失败,恢复策略将保证在回滚过程中,TM有足够的信息来恢复原来的状态。具体来说,可能就是指删除文件或者用以前存在的文件来代替。这种资源的收集操作,使用System.Transactions.Transaction.EnlistDurable方法。
反之,易变资源是不需要”失败恢复”的。同样一个很好的例子,就是持有在内存中的数据。封装这类操作到事务中的RM使用System.Transactions.Transaction.EnlistVolatile方法。
另外一种收集资源的方式是PSPE(可提升的单段式收集)。这种方式由持久的RM拥有事务,当条件改变时,可以逐步升级为由TM管理。因为持久的资源收集比易变的资源收集代价更大,PSPE提供了一种很有吸引力的持久资源收集的实现方式,但是这种方式在许多地方是以牺牲性能为代价的。
在选好资源收集的方式后,接下来你需要实现IEnlistmentNotification接口,以使你从TM获得必要的回调来进行恰当的提交或回滚。 Windows平台的TMs的例子是适合易变资源收集的LTM(轻量级的事务管理器)以及适合分布式或者持久资源收集的MSDTC。
话不多说,下面的例子将向你示范如何实现这些概念。
使用易变资源收集方式实现RM
下面的例子示范了在事务中写成员变量的RM:
public class VolatileRM : IEnlistmentNotification
{
private int memberValue = 0;
private int oldMemberValue = 0;
public int MemberValue
{
get { return memberValue; }
}
public void SetMemberValue(int newMemberValue)
{
Transaction currentTx = Transaction.Current;
if (currentTx != null)
{
Console.WriteLine("VolatileRM: SetMemberValue - EnlistVolatile");
currentTx.EnlistVolatile(this, EnlistmentOptions.None);
}
oldMemberValue = memberValue;
memberValue = newMemberValue;
}
IEnlistmentNotification Members#region IEnlistmentNotification Members
public void Commit(Enlistment enlistment)
{
Console.WriteLine("VolatileRM: Commit");
// Clear out oldMemberValue
oldMemberValue = 0;
}
public void InDoubt(Enlistment enlistment)
{
Console.WriteLine("VolatileRM: InDoubt");
}
public void Prepare(PreparingEnlistment preparingEnlistment)
{
Console.WriteLine("VolatileRM: Prepare");
preparingEnlistment.Prepared();
}
public void Rollback(Enlistment enlistment)
{
Console.WriteLine("VolatileRM: Rollback");
// Restore previous state
memberValue = oldMemberValue;
oldMemberValue = 0;
}
}
正如你所看到的那样,我们在SetMemberValue中首先检查了现在是否在事务中。如果是的话,它如下所示,将自己收集进易变事务中,并且为可能的回滚做一些必要的操作:
public void SetMemberValue(int newMemberValue)
{
Transaction currentTx = Transaction.Current;
if (currentTx != null)
{
Console.WriteLine("VolatileRM: SetMemberValue - EnlistVolatile");
currentTx.EnlistVolatile(this, EnlistmentOptions.None);
}
oldMemberValue = memberValue;
memberValue = newMemberValue;
}
示例中也实现了一系列其他的方法。提交(Commit)、回滚(RollBack),顾名思义,要么提交工作,要么回滚工作。TM调用Prepare方法来通知RM它正请求第一阶段的事务提交工作,并且测试你的RM是否为工作做好了准备。这里,你有下面三种选择:
1.调用preparingEnlistment.Prepare()方法。
这通知TM你已做好准备,并且正等待下一步提交的通知。
2.调用preparingEnlistment.ForceRollBack()方法或者指定preparingEnlistment.RecoveryInformation。
在重新收集资源的时候,这将提供新的RM实例和完成恢复工作所需要的充足的信息。但是现在你还不必为此担心。
3.调用preparingEnlistment.Done()方法。
这将RM作为旁观者。只观察事务,但并不参与其中。通过调用Done方法,TM不会给你第二阶段的通知。
既然你已经建好了RM,你现在能够依赖框架来编写简单可靠的代码。使用上面的RM是相当轻松惬意的一件事情。例如,键入下面的代码:
VolatileRM vrm = null ;
using (TransactionScope txSc = new TransactionScope())
{
vrm = new VolatileRM();
vrm.SetMemberValue(3);
txSc.Complete();
}
Console.WriteLine("Member Value:" + vrm.MemberValue);
上面代码产生下面的输出:
VolatileRM: SetMemberValue - EnlistVolatile
VolatileRM: Prepare
VolatileRM: Commit
Member Value:3
如你所见,成员值被更新了,并且RM在准备和提交阶段得到了相应的通知。
如果你想通过RM实现强制回滚,你可以如下简单的修改RM的Prepare方法:
public void Prepare(PreparingEnlistment preparingEnlistment)
{
Console.WriteLine("VolatileRM: Prepare");
preparingEnlistment.ForceRollback();
}
这将引起如下异常:
The Transaction Has Aborted.
事实上,你可以通过适当的preparingEnlistment.RollBack重载方法引起自定义的异常。
另外一种引起回滚的方法是简单的删掉TransactionScope.Complete语句或者在同一个事务中收集其他引起回滚的RM。
VolatileRM vrm = null ;
using (TransactionScope txSc = new TransactionScope())
{
vrm = new VolatileRM();
vrm.SetMemberValue(3);
// txSc.Complete();
}
Console.WriteLine("Member Value:" + vrm.MemberValue);
当你运行这段代码的时候,将产生如下输出:
VolatileRM: SetMemberValue - EnlistVolatile
VolatileRM: Rollback
Member Value: 0
你可能觉得这个范例有点不知所云。其实,你通过这个范例可以看到,你能够如此有效的创建与其他RM协同工作的事务代码。例如,你能够将这个非数据库操作的修改成员变量的操作同数据库的事务操作放在同一个事务中。你能够使用如下的T-SQL代码轻松建立数据库:
Create Database Test
Go
Create Table Demo
(
DemoValue varchar(5)
)
然后通过下面的代码将VolatileRM和两个SqlConnection实例收入同一个事务中:
private static string connStr = "Data Source=(local);Initial Catalog=Test;Integrated Security=True";
static void Main(string[] args)
{
VolatileRM vrm = null ;
using (TransactionScope txSc = new TransactionScope())
{
vrm = new VolatileRM();
vrm.SetMemberValue(3);
using (SqlConnection cn = new SqlConnection(connStr))
{
SqlCommand cmd = cn.CreateCommand();
cmd.CommandText = "Insert into Demo(DemoValue) Values ('XXX')";
cn.Open();
cmd.ExecuteNonQuery();
cn.Close();
}
using (SqlConnection cn = new SqlConnection(connStr))
{
SqlCommand cmd = cn.CreateCommand();
cmd.CommandText = "Insert into Demo(DemoValue) Values ('YYY')";
cn.Open();
cmd.ExecuteNonQuery();
cn.Close();
}
Console.WriteLine( "Transaction identifier:" + Transaction.Current.TransactionInformation.DistributedIdentifier);
txSc.Complete();
}
Console.WriteLine("Member Value:" + vrm.MemberValue);
}
框架能够通过PSPE自动识别出你的SqlConnection实例已经参与到事务中。当第二个SqlConnection实例出现时,它将在第二个 cn.Open方法被调用时自动将事务提升至MSDTC。你能够通过”控制面板->管理工具->组件服务”中观察这些操作:
提示: 连接至SQL Server 2005的SqlConnection采用PSPE方式,而连接至SQL Server 2000或更低版本的SqlConnection将采用持久方式,即使在当前事务中仅有一个RM。
当你运行程序时,产生如下输出:
VolatileRM: SetMemberValue - EnlistVolatile
Transaction identifier:c40015f6-5086-4688-b565-c65db1cbc8e7
VolatileRM: Prepare
VolatileRM: Commit
Member Value:3
如你所见,你依然在收集易变资源。但是,如果需要,你的事务将自动提升至MSDTC,并且获得分布式ID。如果你是架构师,这将给予你两个最好的世界:事务集成和可能最好的性能。你获得这些仅仅需要写如下的简单的代码:
Transaction
{
Operation A ;
Operation B ;
....
....
Commit();
}
实现基于System.Transactions的事务
现在你已经具备了基于System.Transactions实现事务处理的基础知识。你学到了各种各样的将资源收集到当前事务中的方法,并且看到了自定义RM收集易变资源是多么的简单。
下载代码
下载相关代码,点击这里。
关于作者
Sahil Malik熟悉从Dos至.Net等许多尖端的微软科技。他是Pro ADO.NET 2.0的作者以及Pro ADO.NET with VB.NET 1.1的合著者。Sahil目前也在基于ADO.NET 2.0的多媒体领域有所建树。他曾被授予微软MVP称号。