WCF从理论到实践(12):事务

本文目的

通过阅读本文,您能了解以下知识

1) 如何在WCF中实现事务?

2) 谈谈事务隔离方式的相关知识

3) 事务的实现会给我们编程带来什么样的阻力?

4) 一笔带过,WCF是如何实现分布式事务的?

5) 代码不骗人,用一个银行存取款的范例来演练WCF事务


本文适合的读者

本文适合WCF初学者,学习本文之前,您最好阅读一下WCF从理论到实践系列文章的前几篇


如何在WCF中实现事务?

事务原本是一件难于实现的事情,可WCF总是能化腐朽为神奇,它能够通过简单的声明式编程方式,便可以实现分布式的事务,下面就来看下实现此目标的功臣:
 
1)  TransactionFlowAttribute:操作契约(OperationContractAttribute)的一个属性,它能够指示所属操作(Operation)的事务选项(TransactionFlowOption)。

2)  TransactionFlowOption:它是TransactionFlowAttribute构造函数中的参数,是一个枚举(enum),包括三个枚举项NotAllowed:不允许事务,是缺省值;Allowed:允许事务,意味着事务可有可无;Mandatory:强制事务,表示事务是必须的。

3) TransactionAutoCompleteOnSessionClose:它是操作行为(OperationBehaviorAttribute)的一个属性,用于标注事务完成之后,服务端实例是否自动释放,这个属性和服务对象实例模式紧密相关,使用的时候,应该着重小心,下面也会有说明。

4) TransactionIsolationLevel:也是操作行为(OperationBehaviorAttribute)的一个属性,用于指示事务隔离方式(isolation level).包括5中选项:Any,ReadUncommitted,ReadCommitted,RepeatableRead,Serializable,事务的隔离方式相关知识比较复杂,留作文章的第二点中将具体阐述

5) TransactionTimeout:也是操作行为(OperationBehaviorAttribute)的一个属性,用于指示事务的超时时间,默认为TimeSpan.Zero,表示不会受超时时间的限制..:: OperationBehaviorAttribute:也是操作行为(OperationBehaviorAttribute)的一个属性,用于指示分布式事务选项,如果设置为true,那么必须在事务范围(Transaction Scorp)内。
 
通过上面几个属性的使用,我们便能够轻松的在WCF中实现事务以及分布式事务。具体实现方式可以参考代码范例


谈谈事务隔离方式的相关知识

单纯的事务存在着下面三个问题

1) 脏读:简单的说就是事务一在某一时刻更改了数据,恰恰这个更改的数据被事务二读取,而事务一却最终失败,导致数据回滚,那事务二就是一个受骗者

2)
非可重复性读取:同一数据每次读取的结果都不一样就是非可重复读取。比如事务一要读取的数据务二改变,这就是非可重复性读取

3) 幻读:很简单,比如事务一在查询数据中,事务二却插入了一个符合查询条件的数据,这样就造成新插入数据的幻读。

三种问题的解释可能比较难于理解,其实简单的说,脏读是读了别人正在更改的数据,而可重复读取是更改了别人正在读的数据,而幻读呢则是读了别人还未来得及插入,更改或者删除的数据,我想这样理解就简单多了。

针对上面不同的问题,可以设置事务的不同隔离方式来防止问题的发生,事务的隔离方式又包括

1) Any: 组件的隔离级别是从调用组件的隔离级别获得的。如果组件是根组件,则隔离级别用于 Serializable 中。

2) ReadUncommitted:读取未提交数据,该方式在读取数据时保持共享锁定以避免读取已修改的数据,但在事务结束前可以更改这些数据,这导致非可重复读取或幻读。

3)
ReadCommitted:读取提交数据, 发出共享锁定并允许非独占方式的锁定。该方式与读取未提交数据相相似,这种方式看似和读取未提交数据相似,但有一个区别,事务的只读锁在移到下一行的时候,会解锁,而写入锁却只有在事务完成或者被中止后才解锁,事务要等待所有写入锁解锁。

4) RepeatableRead:可重复性读取,与读取提交数据相似,在查询中使用的所有数据上放置锁,以防止其他用户更新这些数据。防止非可重复读取,但幻读行仍有可能发生。该方式是只读锁也要等到事务结束或者中止才解除

5) Serializable:在完成事务前防止更新或插入。

从上面的描述看,几种隔离方式比较难于区分,但它们能解决事务面临的不同问题,也许记住这些更方便的了解隔离方式

隔离级别

ReadUncommitted

ReadCommitted

RepeatableRead

Serializable

脏读

Yes

No

No

No

非可重复性读取

Yes

Yes

No

No

幻读

Yes

Yes

Yes

No

从上表可以看出隔离方式除了Any之外,一级比一级严厉。Any是一脉相承的,如果它没有可继承的,它将是最严厉的Serializable


事务的实现会给我们编程带来什么样的阻力? 

1) 指定了TransactionFlow(TransactionFlowOption.Mandatory),而Binding却没有设置TransactionFlow为true 此时会出现类似"Bank"协定上至少有一个操作配置为将 TransactionFlowAttribute 属性设置为"强制",但是通道的绑定"WSDualHttpBinding"未使用 TransactionFlowBindingElement 进行配置。没有 TransactionFlowBindingElement,无法使用设置为"强制"的 TransactionFlowAttribute 属性。这样的错误提示。

 
2)
设置了[OperationBehavior(TransactionScopeRequired=true)]的操作,却没有在TransactionScorp中执行,会发生类似"服务操作需要事务成为流"的异常,截图如下:

 3) 也许上面两个问题都是不是问题的问题,那这一点的确是需要我们研发人员注意的,否则我们会吃亏不少。这点涉及到事务和服务实例模式的联系,我们通过学习WCF从理论到实践:实例模式和对象生命周期 我们都学习到了实例在PerSession或者Single模式下不会每次都创建和消亡,这的确是一个不争的真理,可在这里却受到了挑战,不信你可以编写一个程序,即使用你最放心的Single模式,那时不是就是说服务实例是一次创建,终身受用呢?下面就看看我第一次编写范例程序后得到的运行结果,我如下定义Bank

可在调用的时候,我却发现了一个很奇怪的问题,按照理论来说,Bank服务实例应该只创建一次,可运行的截图却是如下:

 

这个结果是不是令大家大失所望呢?这是因为实现了事务的得服务还要受到TransactionAutoCompleteOnSessionClose的限制,该属性默认情况是true,它指示WCF在事务操作完成之后强制销毁服务实例,相当于调用服务的Dispose()方法进行释放,尽管是PerSession或者Single都难逃它的法网。如果想维持实例模式,可以将其设置为false,更改后运行效果便可如期望一致效果图不再给出。

4)
本文更多的实现了分布式任务,分布式任务要使用TransactionScrop这个类,有关它的一些注意事项可以参考我以前的这篇文章:两种实现事务方法的比较


一笔带过,WCF是如何实现分布式事务的?

WCF实现事务的依据是基于WS-AtomicTransaction标准,比较底层的东西,应该属于XXX内幕系列,本文不作详细阐述,如有兴趣,可以查看园子中Kevin Li 兄弟的几篇文章,我列在下面,希望Kevin Li不要见怪

1) 基于WS-AtomicTransaction标准的WCF远程分布式事务(一)

2) 基于WS-AtomicTransaction标准的WCF远程分布式事务(二)

3) 基于WS-AtomicTransaction标准的WCF远程分布式事务(补充)

代码不骗人,用一个银行存取款的范例来演练WCF事务

制作了一个小范例,用于演示事务的实现,示例的情形如下

存在两个服务:1)IATMServer:它是模拟银行的ATM机器的服务 2)IBank,它模拟银行的账户服务。建立一个客户端用于模拟ATM机,我们的操作步骤如下:

1) 插卡

2) 存款

3)
取款

4) 拔卡

操作比较简单,不做赘述,需要注意的是当取款的时候,如果取款金额大于余额的时候,系统会抱错,而账户余额不会更改。运行效果图

项目源文件: /Files/jillzhang/Jillzhang.Wcf.DistributedTranscations.rar


参考资料

1) http://www.rainsts.net/article.asp?id=479

2) http://blog.csdn.net/honkerhero/archive/2007/03/30/1546429.aspx

3) http://www.rainsts.net/article.asp?id=443

作者:jillzhang
出处:http://jillzhang.cnblogs.com/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
标签: WCF, 事务
posted @ 2008-03-30 21:54 Robin Zhang 阅读(7622) 评论(19) 编辑 收藏

 回复 引用   
#1楼 2008-03-30 22:51 gakaki[未注册用户]
请问如果是一个分店的数据要传给总店这样的是不是只能用MSMQ了
银行卡这种如果事务失败,可以提醒操作 的人手动重试,但是像餐饮之类的数据报表上传的话是需要自动的不断重试的!
sql2005 有没有这样的功能

 回复 引用 查看   
#2楼 2008-03-31 00:34 Jeffrey Zhao      
其实WCF的事务其实就是指针对资源修改的事务,而不一定是指数据库的。而幻读等都是数据库特有的,个人感觉应该分开讨论,呵呵。
 回复 引用 查看   
#3楼[楼主] 2008-03-31 08:56 jillzhang      
@gakaki
这种应用情形用MSMQ更适合一些,和事务的应用不太一致,有A和B两个服务,客户端的逻辑确实A中C和B中的D二者必须同时成功,或者同时失败,那么此时适合事务,而MSMQ更多的适用于客户端和服务端要有稳定可靠的数据传输或者离线处理,我理解MSMQ适合C-S这样的情形,而事务适合C-(S1,S2)这样的情形

 回复 引用 查看   
#4楼 2008-03-31 10:02 lbq1221119      
WCF不是太懂的说...学习ing...
 回复 引用 查看   
#5楼 2008-03-31 12:56 dada7357      
刚下了程序来试,出现“HTTP 无法注册 URL http://+:80/Temporary_Listen_Addresses/2fc27fd0-3e1e-4e37-8a78-4dd2406ae668/,因为另一应用程序正在使用 TCP 端口 80。


明明是没有用80端口,但为什么出说80端口被占用?

 回复 引用 查看   
#6楼[楼主] 2008-03-31 13:29 jillzhang      
@dada7357
程序中没有使用80端口亚

 回复 引用 查看   
#7楼[楼主] 2008-03-31 13:29 jillzhang      
@lbq1221119
学习学习就熟悉了,:)

 回复 引用 查看   
#8楼 2008-03-31 13:44 dada7357      
我知啊!但就是不知道为什么在Clinet里调用Login方法时,就是有这句!我都不知道是什么回事
 回复 引用 查看   
#9楼[楼主] 2008-03-31 13:48 jillzhang      
@dada7357
范例程序没有经过任何更改么?我在做事件广播和回调的时候倒是遇到过你说到俄异常,可这个范例没有此问题呀

 回复 引用 查看   
#10楼 2008-03-31 15:49 IamV      
.NET中有好多的Transactions
比如ADO.NET中的Transactions
还有System.Transactions.TransactionScope
到底有什么不同啊?

 回复 引用 查看   
#11楼 2008-03-31 16:42 dada7357      
cleanup_tool.exe这个工具把所有的东西都卸装!程序可以运行啦!
这个是上Google,好似是话WCF和微软的其它产品有冲突!

 回复 引用 查看   
#12楼[楼主] 2008-03-31 22:24 jillzhang      
@IamV
System.Transactions.TransactionScope 提供了一种更为灵活方式

 回复 引用   
#13楼 2008-04-18 11:00 年[未注册用户]
我也出现了
“刚下了程序来试,出现“HTTP 无法注册 URL http://+:80/Temporary_Listen_Addresses/2fc27fd0-3e1e-4e37-8a78-4dd2406ae668/,因为另一应用程序正在使用 TCP 端口 80。
” 明明是没有用80端口,但为什么出说80端口被占用?“这个错误,

网上查了一下,发现在客户端加上如下代码就可以解决问题:

bind.ClientBaseAddress = new Uri("http://localhost:8000/myClient/");

具体见这篇文章
http://kennyw.com/indigo/43

 回复 引用 查看   
#15楼 2008-11-25 10:03 Kingly      
在园子里看了不少你的文章,感觉真不错。尤其是WCF系列的,因为一直也很关注这个,最近做项目遇到了服务之间相互调用的问题,入库服务会调用库存服务增加库存和调用账款服务改变账款,会涉及到事务,请问有没有做过,如何实现的?谢谢
 回复 引用 查看   
#16楼 2009-02-10 09:55 JL      
2) ReadUncommitted:读取未提交数据,该方式在读取数据时保持共享锁定以避免读取已修改的数据,但在事务结束前可以更改这些数据,这导致非可重复读取或幻读。
--
这个地方有问题:
读取未认可的方式是不对读取的资源放置共享锁,所以他可以脏读、非可重复读取或幻读。

 回复 引用 查看   
#17楼 2011-09-13 17:16 细雨黄昏      
我看了一下您提供的源码示例,有两个疑问:
1、对于示例代码而言,即便不使用事务,在调用_bank.Sub(_card, amount);失败的情况下,余额也不会发生变化,因为Bank服务中修改余额的代码:table[card] = money - amount;并没有执行;
2、假如有两个服务方法:A、B,在两个方法中都有对数据库进行操作,客户端顺序调用了A、B两个方法,按我对事务的理解,假如A成功、B失败,那么应该对A所作出的修改进行回滚才对,这个,在示例中似乎体现不出来吧。