使用 WS-AtomicTransaction 的事务性 Web 服务






3.91/5 (11投票s)
解释了一种使用 WS-AtomicTransaction 在 Web 服务之间创建分布式事务的简单方法。
引言
在企业系统中,事务性可能非常重要。一个经典的例子是银行账户之间的转账。金额必须从一个账户中扣除,然后添加到另一个账户中。如果在此过程中的任何时候发生故障,整个过程都应该回滚,就像从未发生过一样。如果在处理过程中服务器发生故障,工作应该保存下来,以便当这些服务器恢复正常时,它们可以继续或回滚。这就是 ACID 原则。ACID 代表原子性(Atomic)、一致性(Consistent)、隔离性(Isolated)和持久性(Durable)。原子性意味着事务是一个完整的工作单元。一致性意味着结果是可预测的。隔离性意味着此工作单元不依赖于其他地方的某些其他工作。持久性意味着如果在任何时候出现问题,事务都可以恢复。有些事情必须以事务性方式完成。
.NET 2 框架引入了 System.Transactions
命名空间。在此之前,.NET 中的事务必须是同构的,比如数据库事务,或者通过 System.EnterpriseServices
分布。企业服务相当庞大,需要了解 COM+ 组件的构建和安装方式。但使用 .NET 2,您可以通过以下方式简单地创建分布式事务:
using (TransactionScope scope = new TransactionScope()) {
// Execute against database 1
// Execute against database 2
scope.Complete();
}
但是,如果您想通过 Web 服务分发事务怎么办?有一种方法,但很棘手。您必须回到 System.EnterpriseServices
和 COM+ 来完成。COM+ 有一个称为事务互联网协议的功能,可用于跨系统分发事务。查看[^] 我之前关于 TIP 的文章,如果您好奇的话。
在我写完关于 TIP 的文章后,微软的项目经理 Jack Loomis 与我联系,谈到了 Indigo (WCF) 中对 WS-AtomicTransaction 的新支持。他希望我使用 WS-AT 重写我的文章。WCF 中的这项新功能比 TIP 甚至在测试阶段时都容易得多。现在,它使用起来要容易得多。我希望通过本文向您展示,在客户端创建事务、调用 Web 服务并让该 Web 服务参与客户端启动的事务是多么简单。
创建数据库
第一步是创建一个数据库进行测试。我们大多数人应该都有 SQL Server 或 SQL Server Express 可用。我相信 Express 将支持分布式事务,但某些较轻量级的 SQL Server 版本不支持。您当然也可以使用 DB2、Oracle、MySql 等。本文随附的代码假定您已经有一个数据库和其登录名。有一个简单的 SQL 脚本可以创建一个名为 MyCategory
的表。MyCategory
表有三列:CategoryId
(标识列)、CategoryName
和 Description
。此表只是一个示例,它或脚本没有什么特别之处。
服务合同
下一步是勾勒出我们的 WCF 服务的服务契约会是什么样子。包含的源代码有一个名为 Common
的程序集,其中包含服务契约接口。这个接口在客户端和服务器之间共享,只是为了减少生成的代码量,并使我的代理类保持简单。您当然不必这样做。您可以在客户端创建 Web 引用。
服务契约将包含创建类别、删除类别和获取所有类别列表的方法。
using System.Collections.Generic;
using System.ServiceModel;
namespace Common
{
[ServiceContract]
public interface ITransactionalWebService
{
[OperationContract]
[TransactionFlow(TransactionFlowOption.Mandatory)]
int CreateCategory(Category category);
[OperationContract]
[TransactionFlow(TransactionFlowOption.Mandatory)]
void DeleteCategory(int categoryId);
[OperationContract]
List<Category> GetAllCategories();
}
}
创建或删除类别时,我希望这些操作被视为事务的一部分。因此,我添加了 TransactionFlow
属性,并将 TransactionFlowOption
设置为 Mandatory
。这意味着操作必须在事务流下调用。事务流在配置中启用,这将在本文后面显示。
上述契约中使用的 Category
类非常简单。它包含所有 public
字段。我原计划使用带有 public string Name { get; set; }
语法的属性,但我想将源代码仅限于 .NET 3。以下是 Category
类:
using System.Runtime.Serialization;
namespace Common
{
[DataContract]
public class Category
{
[DataMember]
public int CategoryId;
[DataMember]
public string Name;
[DataMember]
public string Description;
}
}
事务性 WCF 服务
下一步是创建服务本身并使其实现上述服务契约。
[ServiceBehavior]
public class TransactionalWebService : ITransactionalWebService
{
[OperationBehavior(TransactionScopeRequired = true)]
public int CreateCategory(Category category) { ... }
[OperationBehavior(TransactionScopeRequired = true)]
public void DeleteCategory(int categoryId) { ... }
[OperationBehavior]
public List<Category> GetAllCategories() { .. }
}
上述代码与非事务性服务的唯一区别是 OperationBehavior
上的 TransactionScopeRequired
设置。这表示您所想的意思。这个标志不需要设置为 true
。操作始终可以有或没有事务地工作。出于本示例的目的,我们将强制需要事务范围,这样它将要么加入“流入”的事务,要么创建一个事务来执行操作。
服务 Web.Config
web.config 的配置设置需要对绑定进行一些更改。
<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
<system.serviceModel>
<services>
<service name="TransactionalWebService"
behaviorConfiguration="ServiceBehavior">
<endpoint address="" binding="wsHttpBinding"
bindingConfiguration="Binding1"
contract="Common.ITransactionalWebService"/>
<endpoint contract="IMetadataExchange" binding="mexHttpBinding"
address="mex" />
</service>
</services>
<bindings>
<wsHttpBinding>
<binding name="Binding1" transactionFlow="true">
</binding>
</wsHttpBinding>
</bindings>
<behaviors>
<serviceBehaviors>
<behavior name="ServiceBehavior" returnUnknownExceptionsAsFaults="True">
<serviceMetadata httpGetEnabled="true" />
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
</configuration>
这里最重要的设置是绑定上的 transactionFlow
设置必须设置为 true
。
客户端代理
接下来,创建一个代理以从客户端调用服务。我没有添加 Web 引用,而是创建了我自己的自定义代理,以将所有内容保持在最低限度。代码非常简单:
using System.Collections.Generic;
using System.ServiceModel;
using Common;
namespace ClientSide
{
public class TransactionalWebServiceProxy : ClientBase<ITransactionalWebService>,
ITransactionalWebService
{
public int CreateCategory(Category category)
{
return base.Channel.CreateCategory(category);
}
public void DeleteCategory(int categoryId)
{
base.Channel.DeleteCategory(categoryId);
}
public List<Category> GetAllCategories()
{
return base.Channel.GetAllCategories();
}
}
}
默认构造函数通常意味着它将直接从配置文件获取其信息。配置文件如下所示:
<configuration>
<system.serviceModel>
<bindings>
<wsHttpBinding>
<binding name="WSHttpBinding_TransactionalService"
transactionFlow="true">
</binding>
</wsHttpBinding>
</bindings>
<client>
<endpoint address="https:///WsatTest1WebService/Service.svc"
binding="wsHttpBinding"
bindingConfiguration="WSHttpBinding_TransactionalService"
contract="Common.ITransactionalWebService"
name="ITransactionalWebService">
</endpoint>
</client>
</system.serviceModel>
</configuration>
请注意,客户端绑定上的事务流也设置为 true
。
测试程序
源代码中包含一个测试控制台应用程序。它创建一个事务范围并调用代理以在数据库上执行一些数据操作。一个测试只是一个试金石测试,以确保一切正常。第二个测试创建一个类别,然后尝试删除一个不存在的类别(服务将其视为错误)。以下是第二个测试用例的工作原理。
int origNumRows = -1;
try
{
using (TransactionalWebServiceProxy proxy = new TransactionalWebServiceProxy())
{
// Get the original number of categories from the table.
origNumRows = proxy.GetAllCategories().Count;
// Notice that we've already used the proxy without flowing a transaction
// to call GetAllCategories. To call the Create and Delete methods, we
// need to have a transaction scope because we declared it as mandatory.
using (TransactionScope scope = new TransactionScope())
{
// Create a normal category. The service code will close the connection
// after its done. So, if it didn't participate in the distributed
// transaction, then there will be a category record out there that
// doesn't belong.
Category category = new Category();
category.Name = "I don't belong";
category.Description = "Delete Me";
category.CategoryId = proxy.CreateCategory(category);
// Try to delete something which doesn't exist.
proxy.DeleteCategory(666);
// We should never get to the line below since the above method call
// should throw an exception.
scope.Complete();
}
}
}
catch {} // We're expecting an exception to occur.
// The exception above cause the channel to be faulted. If we used the proxy object
// created above and called another method on it, it would fail with a message indicated
// that the channel has faulted.
using (TransactionalWebServiceProxy proxy = new TransactionalWebServiceProxy())
{
int newNumRows = proxy.GetAllCategories().Count;
if (newNumRows != origNumRows)
Console.WriteLine("Failure");
else
Console.WriteLine("Success");
}
摘要
MSDN 和互联网上有很多关于如何在 WCF 中编写事务性 Web 服务的信息,所以我明白这篇文章不再涵盖一个新主题。然而,当它最初在 2006 年 3 月编写时,它是新颖的。从那时起,很多事情都发生了变化,本文的内容随着新版本的发布而失效。在网络搜索中发现一堆旧信息令人沮丧,因为它会让你走上一段弯路。所以,我决定更新这篇文章,以确保我不会给任何人(其他人)带来更多麻烦。
延伸阅读
还有一个很棒的页面,其中包含更多关于Windows 中的事务管理[^]的信息。
历史
- 1.0: 2006-03-22: 初次修订
- 2.0: 2007-01-30: 更新以匹配 .NET 3 的最终测试版
- 3.0: 2008-06-12: 再次更新以适应 .NET 3 的发布版本