【前言】
大部分程序通常需要确保其操作数据的一致性。通过事务可以帮助达到该一致性。一个事务指一个组工作或一系列操作的原子性,原子性意味着要么全部成功地执行,要么当某个异常发生时全部均不执行。事务的典型例子是在两个银行账户之间转账,即从一个账户上扣除一定数量的金钱并将同样数量的金钱添加至另外一个账户。如果添加操作失败,那么扣除操作也必须复原,否则原账户的金钱丢失。相应地,如果扣除操作失败,添加操作则绝对不会发生。传统意义上讲,事务与数据库系统联系在一起;但从学术意义上讲,事务可以应用到涉及改变数据的任何一组操作之上。
在SOA环境中,事务可以跨越多个服务,可能运行在不同组织的不同的计算机上;这就是所谓的分布式事务。在这种环境下,基础架构必须保证跨越网络的一致性和各种数据存贮之间的一致性。确保该任务是一项非常复杂的工作,因为在网络环境中有各种可能发生失败的地方--该问题已经延伸出许多研究课题。处理分布式事务时,普遍认可的标准有两个:
- OASIS组织提供的Web Service Atomic Transaction (WS-AtomicTransaction) 规范,该规范描述了一个在Web服务架构中处理事务的标准。WS-AtomicTransaction规范定义了在Web服务之间的两阶段提交协议。 从事务的观点看,遵守WS-AtomicTransaction规范架构的Web服务应之间可以互相操作。
- 当创建Web服务时,WS-AtomicTransaction规范相当有用。但是,WCF并非仅仅局限于Web服务;你还可以使用WCF创建基于其他许多技术的应用程序,比如COM,MSMQ和.net framework Remoting。微软也提供了事务管理特性,这些特性集成到当前的Windows操作系统家族中的分布式事务器(DTC)名下,DTC使用其自身优化过的事务协议。基于DTC事务协议的事务称为OLE事务(OLE是COM前身的名字)。如果使用微软的技术创建方案,那么使用OLE事务是不二之选。
.NET Framework 4.0在System.Transactions命名空间下提供一系列的类,结构,接口,代理和枚举。这些类型为WCF的事务管理特性提供一个接口,通过该接口你编写具体的实现代码,而且这些实现代码与控制事务的代码是相互独立的。在本章,你将看到如何创建支持事务的WCF服务,以及如何创建客户端程序初始化事务以及控制事务。
【正文】
在WCF服务中使用事务
在前面章节中实现的ShoppingCartService服务,允许用户添加商品到购物车,但是该服务并没有执行产品程序应包含的一致性检查。比如,服务假设用户向购物车中添加商品时,该商品在仓库中总是存在的。类似地,ShoppingCartService并没有尝试更新存货数量。在本小节接下来的练习中,你将要纠正该服务的这些缺点。你将使ShoppingCartService服务支持事务并确保任何对数据的改变都将保持一致性。
在ShoppingCartService服务中实现OLE事务
你将从学习如何通过TCP端点配置WCF服务使用事务开始。使用TCP协议建立的端点可以支持OLE事务。
在ShoppingCartService服务中启用事务
1. 使用Visual Studio,打开位于WCF\Step.by.Step\Chapter9文件夹下的ShoppingartService
该方案包含第七章中创建的非持续性ShoppingCartService项目,ShoppingCartHost项目和ShoppingCartClient项目。
2. 添加引用System.Transactions到项目ShoppingCartService。该组件包含用以管理事务的一些类和特性。
3. 打开ShoppingCartService项目下的ShoppingCartService.cs文件
4. 添加下面的using语句
using System.Transactions;
5. 为ShoppingCartService添加如下的特性
using System.Transactions;
5. 为ShoppingCartService添加如下的特性
TransactionIsolationLevel属性确定数据库管理系统并发事务如何重叠。在一个系统中,多个并发用户可以在同一时刻访问数据库;然而,当两个用户同时试图修改相同的数据时,或一个用户试图查询另外一个用户正在修改的数据时,都将导致问题。你必须确保并发用户不能反向地互相影响,也就是说它们必须被相互隔离。一般地,无论用户在一个事务中修改、插入或删除数据,直到事务完成之前数据库管理系统都会将受影响的数据锁住。如果事务提交,数据库管理系统将执行该事务的更改。如果错误发生或者事务回滚,数据库管理系统将恢复事务对数据所做的更改。TransactionIsolationLevel属性指定当一个修改数据的事务影响其他尝试读取相同数据的事务时如何应用锁。隔离级别如下表所示:(粗体为常见的隔离级别)
成名名称
|
说明
|
ReadUnCommitted
|
该隔离级别允许一个事务读取其他事务未提交但已经修改的和锁定的数据。该隔离级别提供了最高的并发性;但是,如果其他事务回滚了事务,那么用户可能面临"脏读"的风险。
|
ReadCommitted
|
该隔离级别阻止一个事务读取其他事务未提交但已修改的数据。读取事务被强制处于等待状态,直到被修改的数据被释放。尽管该隔离级别阻止脏读,但它不能保证一致性。如果事务读取相同数据两次,那么有可能另外一个事务在两次读取之间已经修改了数据,因此读事务可能读取到两个不同版本的数据。
|
RepeatableRead
(推荐使用) |
该隔离级别与ReadComitted非常相似,但是该隔离级别使读取事务锁定数据直到读取完成。 这种隔离级别下,事务可以安全地读取多次相同的数据,在这期间,读取事务所读取的数据不会被其他事务修改。该隔离级别因此提供更多的一致性,但是减少了并发性。
|
Serializable
|
但隔离级别比RepeatableRead更进一步。当使用RepeatableRead隔离级别时,一个读取事务读取的数据不能改变。但是,很有可能该读取事务会执行相同的查询两次,并且获取不同的查询结果, 如果另外一个事务插入了匹配查询条件的数据,该结果将会导致:第二次查询多出新插入的数据行。 序列化查隔离级别通过限制其他并发事务添加数据以阻止这种不一致情况的发生。该隔离级别提供了最大程度的一致性,但是并发性也受到显著的影响。
|
Snapshot
|
可以读取可变数据。在事务修改数据之前,它验证在它最初读取数据之后另一个事务是否更改过这些数据。如果数据已经更新,则会引发错误。这样使事务可获取先前提交的数据值
|
Chaos
|
无法覆盖隔离级别更高的事务中刮起的更改
|
Unspecified
|
正在使用与指定隔离级别不同的隔离级别,但是无法确定该级别。如果设置了此值,则会引发异常。
|
6. 在ShoppingCartService.cs中,为AddItemToCart指定OperationBehavior特性
你将更改AddItemToCart方法以使该方法检查选中产品的库存数量,并且如果该产品存在那么修改其库存数量。上述工作将放在同一个事务中;客户端程序应该通过事务调用该操作;这将确保如果发生错误,恢复已发生的更改。设置TransactionScopeRequired属性为true将强制该操作做为事务的一部分而执行;其结果,要么客户端程序建立事务,要么当操作运行时WCF运行时自动创建事务。
TransactionAutoComplete属性指定当操作完成时事务将发生什么情况?如果该属性设置为true,那么事务将自动调用commit并提交所有的更改。设置该属性为false将保持该事务处于运行状态;而且该事务所做的更改还未提交。 该属性的默认值为true。 在AddItemToCart方法中,在用户未结算并支付商品前,你不希望提交更改并结束事务,因为你设置该属性值为false。
7.打开IShoppingCartService.cs文件,为该服务的操作添加TransactionFlow特性
在上一步中TransactionScopeRequired属性提到当调用服务的操作时,如有必要,WCF运行时自动创建一个新的事务。在购物车场景中,你希望客户端程序创建自己的事务,然后在这些事务中调用ShoppingCartService服务的操作。 通过制定TransactionFlowOption属性的值为Mandatory你可以强制客户端在调用服务操作前创建一个事务,并且将该事务的详细信息存储在SOAP消息的header中,然后在调用服务的操作时发送至服务。TransactionFlowOption属性还可以设置为Alllowed, 一个客户端将使用自己创建的事务,如果该事务不存在,WCF运行时创建一个新事务。 TransactionFlowOption还有一个值为NotAllowed,它将使WCF运行时抛弃客户端创建的事务,自己创建一个新事务。
8. 你现在可以添加检查和更新库存数量的代码
View Code
privatebool decrementStockLevel(string productNumber)
{
try
{
using (AdventureWorksEntities database = newAdventureWorksEntities())
{
int productID = (from p in database.Products
whereString.Compare(p.ProductNumber, productNumber) == 0
select p.ProductID).First();
ProductInventory productInventory = database.ProductInventories.First(
pi=>pi.ProductID == productID && pi.Quantity > 0);
productInventory.Quantity--;
database.SaveChanges();
}
}
catch
{
returnfalse;
}
returntrue;
}
9. 在AddItemToCart方法中,修改代码以在增加购物车中商品的同时减少库存中产品的数量。
View Code
public bool AddItemToCart(string productNumber)
{
try
{
ShoppingCartItem item = Find(shoppingCart, productNumber);
if (item != null)
{
if (decrementStockLevel(productNumber))
{
item.volume++;
return true;
}
else
return false;
}
else if (decrementStockLevel(productNumber))
{
using (AdventureWorksEntities database = new AdventureWorksEntities())
{
Product product = (from p in database.Products
where string.Compare(p.ProductNumber, productNumber) == 0
select p).First();
ShoppingCartItem newItem = new ShoppingCartItem
{
ProductNumber = product.ProductNumber,
ProductName = product.Name,
Cost = product.ListPrice,
volume = 1
};
shoppingCart.Add(newItem);
return true;
}
}
else
return false;
}
catch
{
return false;
}
}
10. 指定RemoveItemFromCart、GetShoppingCart和Checkout方法的servicebehavior
注意,虽然GetShoppingCart并不会查询或者修改数据库,但是该方法可能在事务中被调用。因此设置该方法不提交事务非常重要。所以,需要指定该方法的TransactionAutoComplete的值为false。另外你不可以只仅仅设置TransactionAutoComplete的值为false而不设置TransactionScopeRequired为true。
当你修改完服务中的代码后,你还应该修改服务端点以使WCF运行时从客户端程序到服务"流动"事务。关于事务的信息包含在客户端调用操作时发送的SOAP消息的header中。
注意:并不是所有的绑定都允许你从客户端想服务流动事务。 不支持该特性的绑定包括BasicHttpBinding, NetMsmqBinding, NetPeerTcpBinding和WebHttpBinding
配置ShoppingCartService服务使其支持从客户端向服务“流动”事务
1. 使用服务配置管理工具编辑ShoppingCartHost项目的app.config文件
2. 在服务配置管理工具中,在配置面板,点击绑定文件夹。然后在右边面板中,点击创建一个新绑定配置
3. 在创建新的绑定对话框,选择netTcpBinding绑定类型,然后点击确认按钮
4. 在右边面板中,更改绑定的名字为ShoppingCartService_NetTcpBindingCfg,然后设置TransactionFlow的属性为True;并确认TransactionProtocol属性设置为OldTransactins。
TransactionFlow属性用以指定服务期望从接收到的SOAP消息中收到关于事务的信息。TransactionProtocol属性指定服务将使用的事务协议。默认情况下,基于TCP传输协议的端点当执行分布式事务时使用内部的DTC协议。然而,你可以修改TransactionProtocol属性的值,设置它们使用遵循WS-AtomicTransaction协议的WSAtomicTransactionOctober2004,或WSAtomicTransaction11
5. 在配置面板中,展开服务文件夹,然后展开ShoppingCartService.ShoppingCartService节点,在展开端点,然后选择未命名节点。在服务端点面板中,确认绑定协议为netTcpBinding;然后设置绑定配置为ShoppingCartService_NetTcpBindingCfg
6. 保存配置文件,并关闭服务配置编辑器
你已经配置ShoppingCartService服务期望客户端在通过事务调用操作。接下来你需要做的是修改创建事务的客户端
在客户端程序中创建事务
1. 在Visual Studio中,添加引用System.Transactions到项目ShoppingCartClient
2. 打开ShoppingCartClient项目的Programm.cs文件,添加下面的using语句
using System.Transactions;
3. 在main方法中,按照下面的方式修改try片段中的代码
using System.Transactions;
3. 在main方法中,按照下面的方式修改try片段中的代码
View Code
try
{
ShoppingCartServiceClient proxy =
newShoppingCartServiceClient("NetTcpBinding_IShoppingCartService");
TransactionOptions options = newTransactionOptions();
options.IsolationLevel = IsolationLevel.RepeatableRead;
options.Timeout = newTimeSpan(0, 1, 0);
using (TransactionScope scope =
newTransactionScope(TransactionScopeOption.RequiresNew, options))
{
proxy.AddItemToCart("WB-H098");
proxy.AddItemToCart("WB-H098");
proxy.AddItemToCart("SA-M198");
string cartContents = proxy.GetShoppingCart();
Console.WriteLine(cartContents);
}
proxy.Close();
}
你可以通过多种方式创建事务:
- 通过设置TransactionScopeRequired属性的值为true,服务将自动地初始化一个新事务;
- 或者通过创建一个可提交的事务对象,操作显示地开始一个新的事务;
- 或者客户端隐式地创建一个新事务。在WCF客户端程序端,推荐的方式是使用TransactionScope对象
当你创建一个新的TransactionScope对象,随后的任何事务性操作都自动地加入到该事务。如果WCF运行时在创建TransactionScope对象发现没有活动的事务可供食用,WCF运行时将初始化一个新事务,并在该事务中执行操作。在这种情况下,直到TransactionScope对象被销毁,该事务将一直处于活动状态。正因为这个原因,代码中显示地使用using代码片段以限制一个事务的存活范围。
TransactionScope构造器中的TransactionScopeOption参数指定WCF运行时如何利用现存的事务。
- 如果该参数设置TransactionScopeOption.RequireNew,WCF运行时将始终创建一个新的事务。
- 如果为TransactionScopeOption.Required那么WCF运行时在没有可用的事务时才创建新事务;否则使用现存的服务。
- 如果为TransactionScopeOption.Suppress,那么TransactionScope中所有的操作不通过事务执行。
无论使用哪种方式,新创建事务的事务隔离级别都应当与服务所要求的事务隔离级别相匹配。你可以通过创建TransactionOptons对象以指定事务隔离级别。你还可以指定事务的延时,该值可以改善一个操作的响应;因为这样事务就不用在一个不确定的时间内等待其他事务释放锁定的资源。实际上,当超时发生时,WCF运行时将抛出一个异常,然后客户端处理该异常。在本例中,事务超时的值为一分钟。
4. 添加下面的if片段:
if (proxy.CheckOut())
scope.Complete();
Console.WriteLine("Goods purchased");
if (proxy.CheckOut())
scope.Complete();
Console.WriteLine("Goods purchased");
默认情况下,当程序离开using片段时,除非你指定其他情况,事务将被抛弃,而且所做的工作都将被复原;这可能不是你所期望的。 在TransactionScope对象被销毁前调用TransactionScope对象Complete方法指明工作已成功地完成,并且应当提交事务。在ShoppingCartService服务中,如果结算操作成功执行那么Checkout方法将返回true,否则返回false;如果返回false,Complete方法将不会被调用,而且由该事务对数据库所作的更改都将回滚。
5. 在前面的练习中,你通过添加TransactionFlow特性修改了ShoppingCartService服务,因此你必须更新客户端代理类。
打开ShoppingCartServiceProxy.cs;然后添加下面的语句
using System.ServiceModel;
然后每个操作上添加TransactionFlow特性
最后一步就是配置客户端的端点,使其通过网络向服务发送关于事务的消息
配置客户端程序使其支持事务
1. 使用WCF服务配置编辑工具编辑ShoppingCartClinet项目的app.config文件
2. 在服务配置编辑工具中,在配置面板,点击绑定文件夹,在右边的面板中,点击创建绑定配置
3. 在创建绑定对话框中,选择NetTcpBinding绑定类型,然后点击确认按钮
4. 在右边面板,更改Name属性为ShoppingCartClient_NetTcpBindingCfg。在面板的常规区域,设置TransactionFlow属性的值为True;确认TransactionProtocol属性为OleTransactions
5. 在配置面板中,选择客户端à端点àNetTcpBinding_IShoppingCartService节点,然后设置该绑定的绑定配置为ShoppingCartClient_NetTcpBindingCfg
6. 保存配置文件,并退出WCF服务配置编辑工具。
现在你可以测试支持事务的ShoppingCartService服务和客户端。
测试ShoppingCartService服务
1. 在系统开始菜单,打开控制面板,点击系统和安全,点击管理工具,然后右键,点击组件服务,然后选择使用管理员运行。如果提示输入用户名和密码,输入相应的用户名和密码。
2. 在组件服务控制台窗口的左边面板,点击服务。在右边面板中,点击分布式事务器,然后点击重启该服务
3. 在左边面板,展开组件服务节点,展开计算机节点,展开我的计算机,展开分布式事务器,展开本地DTC,然后点击事务统计
4. 返回Visual Studio,然后在非调适模式下运行方案
按ENTER键关闭客户端控制台窗口。在宿主程序中,按ENTER键停止服务
5. 切换到组件服务控制台。可以看到一个放弃的事务。
6. 在Visual Studio中,打开ShoppingCartService项目下的ShoppingCartService.cs文件,添加下面高亮的代码。该代码用于在调用Checkout操作时,提交事务。
7. 再一次在非调适模式下运行方案
8. 按ENTER键关闭客户端控制台窗口。在宿主程序中,按ENTER键停止服务
9. 回到组件服务控制台窗口;这次,你将得到如下结果:
10. 确认数据库的已经被更新。以管理员身份运行Visual Studio命令行工具,转到Chapter9文件夹,然后执行StockLevels命令
11. 返回到Visual Studio,再一次在非调适模式下运行项目。然后按ENTER键关闭客户端控制台窗口。在宿主程序中,按ENTER键停止服务
12. 再次返回到Visual Studio命令行工具,在一次执行stocklevel命令。确认库存数量分别减少了两个和一个
13. 在组件服务控制台窗口,检查事务统计。你应当发现有两个提交了的事务
14. 关闭组件服务控制台。
实现WS-AtomicTransaction协议
NetTcpBinding绑定使用OLE事务和微软自有的用于DTC与其他微软特有的服务比如SQL之间的协议。在基于非微软技术服务的各种多元环境中,你应该使用一个更标准的机制,比如WS-AtomicTransaction协议。当使用NetTcpContextBinding,NetTcpBinding,或者NetNamePipeBinding,你可以通过TransactionProtocol属性显示地指定上述绑定使用哪个协议。而HTTP系列绑定,WCF运行时根据Windows的配置选择用户从客户“流动”至服务的事务流的事务协议,传输协议,以及SOAP消息header的格式。
比如,一个客户端通过基于http的端点连接至服务时,将自动使用OLE事务。如果寄宿WCF客户端程序和WCF服务的计算机配置了在特定的端口上支持WS-AtomicTransaction协议,并且客户端程序通过基于https而且使用该特定端口的端点链接至服务,那么事务将遵守WS-AtomicTransaction协议。
选择事务协议应当对服务和客户端都是透明的。基于WS-AtomicTransaction协议,你需要编写初始化、控制、及支持事务的代码与操作OLE事务是一样的。因此,同一个服务既可以使用OLE事务,也可以使用WS-AtomicTransaction事务,这取决于你如何配置该服务。
如果你希望使用由.NET Framwork 4.0提供的HTTP绑定实现WS-AtomicTransaction事务协议,你必须在DTC中配置支持WS-AtomicTransaction协议。
.NET Framework 4.0包含一个waatConfig.exe的实用工具,它位于C:\Windows\Microsoft.NET\Framework\v4.0.30319文件夹下。你可以使用该命令后工具配置支持WS-AtomicTransaction协议。微软WindowsSDK提供了一个图形化用户接口组件以执行许多相同的任务,并且它是组件服务控制台的一个插件,如下图所示。你可以在分布式事务器中,选择本地DTC属性来访问该接口。
注意:必须注册实现该用户接口的组件后才能在组件服务控制台看到该用户接口。注册该用户接口,你需使用管理员身份运行Visual Studio命令行工具,然后执行命令:Regasm /codebase wsatui.dll
为了支持WS-AtomicTransaction,你首先必须配置网络使其支持DTC读取并使其允许DTC上的出入通讯运行,你可以在本地DTC属性窗口的安全面板中执行该任务。在HTTP协议上实现WS-AtomicTransaction协议要求对所有的消息相互验证、以及保证消息的一致性和消息保密性。这意味着你必须配置HTTPS传输协议。如果WCF服务不是在443端口上侦听请求,你应该在WS-AT面板中指定端口。你还必须提供一个用以加密消息的证书。此外,在WS-AT面板中,允许你授权可以访问服务的用户。
如果你想了解更多关于WS-AtomicTransaction与DTC的信息,请参考http://msdn.microsoft.com/en-us/library/ms733943.aspx
【扩展阅读】
1. 在WebSphere Application Server 和Microsoft .NET 中使用WS-AtomicTransaction 构建事务Web 服务 http://www.ibm.com/developerworks/cn/websphere/library/techarticles/0707_lo/0707_lo.html
2. 使用WS-AtomicTransaction 和JTA 的分布式事务http://www.ibm.com/developerworks/cn/webservices/ws-transjta/
3. Web服务事务规范索引页http://msdn.microsoft.com/en-us/library/ms951262.aspx