65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.91/5 (11投票s)

2006年3月22日

CPOL

6分钟阅读

viewsIcon

140371

downloadIcon

512

解释了一种使用 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(标识列)、CategoryNameDescription。此表只是一个示例,它或脚本没有什么特别之处。

服务合同

下一步是勾勒出我们的 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 的发布版本
© . All rights reserved.