分类:
2010-06-23 19:14:33
(原创:灰灰虫的家)
事务(Transaction)是并发控制的基本单位。所谓事务,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。
一、事务特性(ACID):
原子性(Atomic)整个事务是个不可分割的整休,所有操作都只有两种状态:“全执行”或“全不执行”。不能再有其它中间子状态。
一致性(Consistent)系统要么处于事务执行前的状行,要么处理事务成功执行后的状态。要保持数据的完整性,不能产生数据的丢失与叠加。
隔离性(Isolated)每个独立的事务在执行的时候保护各自的资源。不同的事务之间不能产生资源访问并发冲突,通常使用“锁”来实现事务的隔离。
持久性(Durable)当设备失效时操作结果不会损坏事务的一致性。
注:
1.事务隔离的相关知识:
脏读:事务一在某一时刻更改了数据,恰恰这个更改的数据被事务二读取,而事务一却最终失败,导致数据回滚,那事务二就是一个受骗者。
非可重复性读取:同一数据每次读取的结果都不一样就是非可重复读取。比如事务一要读取的数据已经被事务二改变,这就是非可重复性读取 。
幻读:事务一在查询数据中,事务二却插入了一个符合查询条件的数据,这个数据已被事务一读取到,但事务二却还未提交。这样就造成事务一对事务二新插入数据的幻读。 即读了别人还未来得及插入,更改或者删除的数据。
2.两阶段提交:
第一阶段:事务管理器A会向参与事务的其他服务发出调用请求。其它机器拦截请求。获取事务ID,启动本地事务。
第二阶段:各服务表决是否已正确实现本地事务,如果表决全部通过,则分布式事情整体提交。只要有一个服务表决未通过,则分布式事务整体回滚。
二、事务传播的绑定协议
WSHttpBinding、WSDualHttpBinding、WSFederationBinding、NetTcpBinding和NetNamedPipesBinding。
在配置文件中指明使用的绑定协议,并在绑定协议中启动事务流机制
三、事务流
在WCF中事务往往是分布式事务,即需要事务跨越客户端、服务端,甚至跨越多个其它服务端进行执行。因此我们需要把事务对象以流的方式在客户端和服务端进行传递,以使此事务对象能有效地控制整个执行过程中的一致性。
要实现这种分布式的事务流,需要我们对客户端代码和服务端代码都做好事务的设置才能够实现事务流的控制。
服务端事务流的属性设置:
通常我们使用TransactionFlowAttribute、ServiceBehaviorAttribute 和 OperationBehaviorAttribute 这三个特性来指明服务端事务控制。
TransactionFlowAttribute:修饰方法契约。指定该方法在执行的时候是否参与事务
其构造函数接收TransactionFlowOption枚举类型的参数,指明其参与事务的方式。
TransactionFlowOption.NotAllowed:禁止参与事务。(默认值)
TransactionFlowOption.Allowed:允许参与事务,如果调用方启用了事务,则参与。
TransactionFlowOption.Mandatory:强制参与事务,调用方必须启用事务才能调用本服务。
ServiceBehaviorAttribute:描述服务参与事务的一些参数
TransactionAutoCompleteOnSessionClose = bool:指示当会话(Session)结束时是否自动提交事务(Complete); 这个属性和服务对象实例模式紧密相关,使用的时候,应该着重小心。
ReleaseServiceInstanceOnTransactionComplete = bool:指示事务提交后是否释放服务实例对象;
TransactionIsolationLevel= System.Transactions.IsolationLevel:用于指示事务隔离级别.
TransactionTimeout = string:设置事务超时时间。
OperationBehaviorAttribute:描述契约方法如何参与事务的一些参数
TransactionScopeRequired = bool:该属性是 WCF 分布事务所必需的。它表明服务方法必须在事务范围(transaction scope)内执行。如果不添加该标记,则意味着服务方法不参与到事务中。
TransactionAutoComplete = bool: 指示方法正常结束后是否自动提交事务。
四、事务模型
1.Client/Service 事务模型
如名称所言,如果客户端启用了事务,则服务端就参与事务;如果客户端没有启用事务,则服务端独立启用事务。即不管客户端是否启用事务,服务端总是运行在事务中。
实现步骤:
a.选择一个支持事务的绑定,设置TransactionFlow = true。
b.在方法契约上添加TransactionFlowAttribute声明,设置TransactionFlow(TransactionFlowOption.Allowed)。
c.在方法行为上声明OperationBehavior(TransactionScopeRequired=true)。
示例
设计思路:
服务类中,在方法中显示当前事务本地ID,并在事务结束的事件中显示事务全局ID和事务执行的结果状态。客户类中,在客户端中分两次调用服务,第一次调用时客户端不启用事务,第二次的调用被置入分布事务中,以测试服务端代码如何参与事务。
服务端代码:
[ServiceContract]
public interface IClientServiceTransaction
{
[OperationContract]
//如果客户端启用了事务,则服务端参与事务
[TransactionFlow(TransactionFlowOption.Allowed)]
void CSMethod();
}
public class ClientServiceTransaction:IClientServiceTransaction
{
//服务端代码必须置于服务中执行
[OperationBehavior(TransactionScopeRequired=true)]
public void CSMethod()
{
//获取当前事务对象
Transaction transaction = Transaction.Current;
//注册事务流执行结束的事件方法
transaction.TransactionCompleted += new TransactionCompletedEventHandler(transaction_TransactionCompleted);
//显示事务的本地ID
Debug.WriteLineIf(transaction != null,"<服务端>事务本地ID:"+transaction.TransactionInformation.LocalIdentifier);
}
//事务流执行结束时会触发此方法
void transaction_TransactionCompleted(object sender, TransactionEventArgs e)
{
//显示事务全局的ID
Debug.WriteLine("<服务端>事务全局ID:"+e.Transaction.TransactionInformation.DistributedIdentifier);
//显示事务的执行结果
Debug.WriteLine("<服务端>事务执行结果"+e.Transaction.TransactionInformation.Status);
}
}
服务配置代码:
在配置中我建立了两个终结点,分别采用netTcpBinding和wsHttpBinding。
客户端代码:
public static void Maincs(string[] args)
{
SRClientService.ClientServiceTransactionClient prox = new Client.SRClientService.ClientServiceTransactionClient("WSHttpBinding_IClientServiceTransaction");
//第一次调用,客户端没有启用事务
prox.CSMethod();
using (TransactionScope ts = new TransactionScope())
{
Transaction.Current.TransactionCompleted += new TransactionCompletedEventHandler(Current_TransactionCompleted);
Console.WriteLine("<客户端>事务本地ID:"+Transaction.Current.TransactionInformation.LocalIdentifier);
//第二次调用事务,客户端启用了分布式事务
prox.CSMethod();
//事务提交。如果提交,事务流会执行成功;如果不提交的话,事务流会回滚。
//ts.Complete();
}
Console.ReadLine();
}
//事务流执行结束时会触发此方法
static void Current_TransactionCompleted(object sender, TransactionEventArgs e)
{
Console.WriteLine("<客户端>事务全局ID:"+e.Transaction.TransactionInformation.DistributedIdentifier);
Console.WriteLine("<客户端>事务执行结果:"+e.Transaction.TransactionInformation.Status);
}
运行结果:
《图1》
从图中我们看到:
1.服务端第一次执行成功,事务有本地ID,但没有全局ID。这说明虽然客户端调用没有启用事务,但服务端代码仍在事务中运行,该事务是服务端本地事务。
2.客户的全局事务ID与服务端的全局事务ID相同,这说明客户端与服务端处于同一个事务流中。但二者本地的事务ID不同,这说明它们各自是处于全局“大事务”中的本地“小事务”。由此得出,服务端代码仍在事务中运行,并参与到客户端事务流中
3.由于客户端代码中ts.Complete()被注释了,所以客户端事务执行不会提交,从而导致服务端事务提交失败,全局事务流提交失败。
Client/Service事务模型是最常见的,它对客户端准备了“两手应对策略”:
当客户端启动了事务流的话,服务就参与事务流中,以维持整个系统的一致性,这样客户端的操作和服务端的操作会做为一个整体进行操作,任何一处的操作产生异常都会导致整个事务流的回滚。
如果客户端没有启动事务流,服务端的操作仍需要在事务的保护下运行,它会自动启动事务保护。
(车延禄)
2.Client事务模型
Client事务模型必须由客户端启动分布式事务,并强制服务端必须参与客户端事务流。
实现步骤:
a.选择一个支持事务的Binding,设置 TransactionFlow = true。
b.在方法契约上添加TransactionFlowAttribute声明,设置TransactionFlow(TransactionFlowOption.Mandatory)。
c.在方法行为上声明OperationBehavior(TransactionScopeRequired=true)。
示例:
服务端代码:
[ServiceContract]
public interface IClientTransaction
{
[OperationContract]
//强制把当前事务加入客户端事务流中
[TransactionFlow(TransactionFlowOption.Mandatory)]
void TestMethod();
}
public class ClientTransaction:IClientTransaction
{
//服务端代码必须置于服务中执行
[OperationBehavior(TransactionScopeRequired=true)]
public void TestMethod()
{
Transaction transaction = Transaction.Current;
transaction.TransactionCompleted += new TransactionCompletedEventHandler(transaction_TransactionCompleted);
Debug.WriteLineIf(transaction != null, "<服务端>事务本地ID:" + transaction.TransactionInformation.LocalIdentifier);
}
void transaction_TransactionCompleted(object sender, TransactionEventArgs e)
{
Debug.WriteLine("<服务端>事务全局ID:"+e.Transaction.TransactionInformation.DistributedIdentifier);
Debug.WriteLine("<服务端>事务执行结果:"+e.Transaction.TransactionInformation.Status);
}
}
服务配置代码:
客户端代码:
public class ClientTransaction
{
public static void Main(string[] args)
{
//客户端不启用事务,会产生异常,如图2所示
//SRClientTransaction.ClientTransactionClient prox = new SRClientTransaction.ClientTransactionClient();
//prox.Open();
//prox.TestMethod();
//prox.Close();
//客户端启用事务
SRClientTransaction.ClientTransactionClient prox = new SRClientTransaction.ClientTransactionClient();
prox.Open();
using (TransactionScope ts = new TransactionScope())
{
prox.TestMethod();
//获取当前事务对象
Transaction trans = Transaction.Current;
//注册服务结束事件方法
trans.TransactionCompleted += new TransactionCompletedEventHandler(trans_TransactionCompleted);
//显示事务本地ID;
Console.WriteLine("<客户端>事务本地ID:"+trans.TransactionInformation.LocalIdentifier);
//提交事务
ts.Complete();
}
prox.Close();
Console.ReadLine();
}
//事务结束触发的事件
static void trans_TransactionCompleted(object sender, TransactionEventArgs e)
{
Console.WriteLine("<客户端>事务全局ID:" + e.Transaction.TransactionInformation.DistributedIdentifier);
Console.WriteLine("<客户端>事务执行结果:" + e.Transaction.TransactionInformation.Status);
}
}
如果客户端没有启用事务会产生异常信息
《图2》
如果客户端启用事务,则客户端会和服务端会使用同一事务流
《图3》
这种事务模式,服务端不能独立启动事务,服务端事务必须参与客户端事务流中,确保客户端和服务端处于同一事务流中,这样客户端和服务端的代码会做为一个原子执行,当事务流中任何一个环节产生异常都会把整个事务流进行回滚,实现非常好的一致性。
3.Service事务模型
这种事务模型是把服务端事务与客户端事务分离开,服务端代码执行总是会创建自己的事务,并不会参与到客户端事务中去。所以客户端的事务启用与否并不影响服务端事务的执行。
实现步骤:
a.可以使用任何绑定信道。如果选择的是支持事务的绑定,需要设置TransactionFlow = false,因为服务端事务独立启动,并不需要事务流。
b.不需要在方法契约上添加TransactionFlowAttribute声明。如果你非得设置此Attribute的话,请设置TransactionFlow(TransactionFlowOption.NotAllowed),指明服务端拒绝参与事务流。
c.在方法行为上声明OperationBehavior(TransactionScopeRequired=true)。指明服务端需要自己启用事务。
示例:
服务端代码:
[ServiceContract]
public interface IServiceTransaction
{
[OperationContract]
//禁用事务流功能
[TransactionFlow(TransactionFlowOption.NotAllowed)]
void TestMethod();
}
public class ServiceTransaction : IServiceTransaction
{
//服务端代码必须置于服务中执行
[OperationBehavior(TransactionScopeRequired = true)]
public void TestMethod()
{
Transaction transaction = Transaction.Current;
transaction.TransactionCompleted += new TransactionCompletedEventHandler(transaction_TransactionCompleted);
Debug.WriteLineIf(transaction != null, "<服务端>事务本地ID" + transaction.TransactionInformation.LocalIdentifier);
}
void transaction_TransactionCompleted(object sender, TransactionEventArgs e)
{
Debug.WriteLine("<服务端>事务全局ID:" + e.Transaction.TransactionInformation.DistributedIdentifier);
Debug.WriteLine("<服务端>事务执行结果:" + e.Transaction.TransactionInformation.Status);
}
}
服务配置:
客户端代码:
public class ServiceTransaction
{
public static void Main(string[] args)
{
SRServiceTransaction.ServiceTransactionClient prox = new Client.SRServiceTransaction.ServiceTransactionClient();
prox.Open();
using (TransactionScope ts = new TransactionScope())
{
prox.TestMethod();
Transaction trans = Transaction.Current;
trans.TransactionCompleted += new TransactionCompletedEventHandler(trans_TransactionCompleted);
Console.WriteLine("<客户端>事务本地ID:" + trans.TransactionInformation.LocalIdentifier);
//ts.Complete();
}
prox.Close();
Console.ReadLine();
}
static void trans_TransactionCompleted(object sender, TransactionEventArgs e)
{
Console.WriteLine("<客户端>事务全局ID:" + e.Transaction.TransactionInformation.DistributedIdentifier);
Console.WriteLine("<客户端>事务执行结果:" + e.Transaction.TransactionInformation.Status);
}
}
运行结果:
《图4》
从图中我们可以看到:
服务端本地事务ID不为空,说明服务端代码仍处于事务中执行。但全局事务ID都是空的,说明这种事务模型不存在全局型事务流。
还管客户端事务执行是否成功,服务端事务总是成功执行。
这种事务模型一般应用于:当服务端需要在客户端事务流之外独立运行事务的情况。
例一:我们编写一个记录操作日志的服务,客户端所做的每一步操作都需要调用服务端的方法进行记录。这个日志记录服务端就适用于Service服务模型,不管客户端事务是否正常提交,服务端记录日志的事务不受影响。如果采取的是Client/Service事务模型或Client事务模型的话,当客户端事务没有提交成功,会导致服务端日志无法正常记录。
例二:我们设计一个银行存款的程序:
一个客户端:
用户输入界面
两个服务端:
服务端A:实现帐户的改变
服务端B:实现回单的打印
当用户输入存款金额后,会调用服务端A变更帐户,再调用服务端B实现凭证打印,然后在客户端提示交易成功。
这个过程需要事务流进行控制,但是这种控制会产生问题:如果打印机缺纸,就会产生异常,导致事务流的回滚,帐户的修改就会失败;实际应用中,打印机缺纸不应当导致帐户修改的失败,甚至可以不打印回单。这时候我们就需要对服务端A使用Service事务模型,在事务流其它部分回滚不影响帐户的修改。
(车延禄)
4.None事务模型
这种事务模型中服务端代码不启用任何事务也不参与任何事务流。
实现步骤:
a.可以使用任何绑定信道。如果选择的是支持事务的绑定,需要设置TransactionFlow = false,因为服务端事务独立启动,并不需要事务流。
b.不需要在方法契约上添加TransactionFlowAttribute声明。如果你非得设置此Attribute的话,请设置TransactionFlow(TransactionFlowOption.NotAllowed),指明服务端拒绝参与事务流。
c.不需要在方法行为上添加TransactionScopeRequired属性声明,如果你非要设置此属性的话,请设置OperationBehavior(TransactionScopeRequired=false)。指明此方法执行过程中不使用事务。
即默认情况下,服和端是不使用事务的。
这种事务模型就不再做代码演示了。
显然这种事务模型很危险,它破坏了系统的一致性:如果客户端在执行过程中产生异常时,将不会回滚服务端的数据。一般这在一些不会出现不一致性的系统中采用这种模型。
事务模型的选择
上面所述的四种事务模型中,Service模型和None模型用得比较少,这两种模式带系统不一致的潜在危险。一般在需要事务流控制的地方,我们应选择Client/Service模型或Client模型来确保客户端和服务端之间系统一致。
五、事务表决(Transaction Voting)。
如果多个服务参与一个事务流,必须所有服务都执行成功后才能提交整个事务流;只要有一个服务失败整个事务流就会回滚至事务流开始之前的状态。
因此事务流必须知道是否每个服务功能都执行成功,当每个服务功能都执行表决通过时,整个事务流就进行提交,这个过程就是我们所说的“事务表决”,也有人称之为“事务投票”。
WCF提供了两种事务表决的模型:“声明式表决模型”和“编程式表决模型”
1.声明式表决模型
在方法的OperationBehavior特性声明中指定TransactionAutoComplete=true来实现。当方法在执行成功是就自动表决通过,如果方法执行产生异常或终止,则表决不通过。TransactionAutoComplete属性的值默认就是true,因此也可以不用为此属性赋值。
因此下面两段代码是一样的,都实现了声明式表决
[OperationBehavior(TransactionScopeRequired = true,TransactionAutoComplete = true)]
public void MyMethod(...)
{...}
[OperationBehavior (TransactionScopeRequired = true)]
public void MyMethod(...)
{...}
在事务流中我们需要把每个参与的方法的OperationBehavior特性声明为TransactionScopeRequired = true,这样该方法就参与了事务流。但在方法内部尽量避免处理异常。
如:下面这种编码方式应当避免
[OperationBehavior(TransactionScopeRequired = true)]
public void MyMethod(...)
{
try
{
...
}
catch
{
Transaction.Current.Rollback( );
}
}
因为在此方法的catch块中把事务回滚终止了操作,即把异常处理了,那该异常就不会再反升给事务流,事务流就不会捕获到异常信息。但由于该方法产生了异常,事务表决未通过,直接导致整个事务流的回滚。因此就出现了事务流在不明原因的情况下,产生了回滚。
即使你需要在此方法中使用catch捕获异常来实现记录日志等功能,那也应当在catch块结束后再把异常抛给事务流,这样层层上抛直至事务流的根再进行异常处理。
正确的代码应当如下:
[OperationBehavior(TransactionScopeRequired = true)]
public void MyMethod(...)
{
try
{
...
}
catch
{
/* 本地异常处理代码 */
throw; //再把异常信息上抛至事务流
}
}
2.编程式表决模型
编程式表决模型需要把方法的OperationBehavior特性中的TransactionAutoComplete属性设为False,TransactionScopeRequired属性设为true。在方法中使用OperationContext.Current.SetTransactionComplete( )方法来进行事务表决。
代码如下:
[OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)]
public void MyMethod(...)
{
/* 其它操作*/
OperationContext.Current.SetTransactionComplete( );
}
一般来说SetTransactionComplete()要放在最后执行,在SetTransactionComplete()方法之后执行任何事务操作都会导致异常产生。
这种编程式表决模型一般用在:根据其它服务的运行结果来决定是否表决通过此事务。它使用起来比“声明式表决模型”要复杂一些,但更灵活。