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

关于理解事务和创建支持事务的 WCF 服务的教程。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (10投票s)

2013年4月2日

CPOL

7分钟阅读

viewsIcon

45928

downloadIcon

1197

本文讨论了创建支持事务的 WCF 服务所需的步骤。

引言

在本文中,我们将讨论创建支持事务的 WCF 服务。我们将了解 WCF 服务端需要做什么才能支持事务。我们还将通过一个示例应用程序了解客户端应用程序如何使用这些支持事务的服务。 

背景

当我们谈论数据库操作中的事务时,我们需要以一种方式执行某些操作(数据库操作),即要么所有操作都成功,要么所有操作都失败。这将导致在事务成功或失败后,金额信息保持不变。

事务的属性

根据定义,事务必须是原子性(Atomic)、一致性(Consistent)、隔离性(Isolated)和持久性(Durable)的。这些术语是什么意思?

  • 原子性: 原子性意味着交易中包含的所有语句(SQL 语句或操作)都应作为原子操作工作,即全部成功或全部失败。
  • 一致性: 这意味着如果原子事务成功,数据库应该处于反映更改的状态。如果事务失败,数据库应该与事务开始时完全相同。
  • 隔离性: 如果有多个事务正在进行,那么这些事务中的每一个都应该独立工作,并且不应影响其他事务。
  • 持久性: 持久性意味着一旦事务被提交,更改就应该是永久的,即这些更改将保存在数据库中,并且无论发生什么(例如停电或其他情况),都应该持续存在。

现在,从一个简单的类库或简单的 .Net 应用程序的角度来看,事务只是在事务中调用操作,检查所有操作是否成功,并决定是提交还是回滚事务。有关普通应用程序中事务的信息,请参阅本文:初学者理解 ADO.NET 中的事务和 TransactionScope 的教程[^]

但是,从 WCF 服务的角度来看,由于服务本身可能运行在远程服务器上,并且服务与客户端之间的所有通信都是消息形式的,因此服务本身需要一些配置才能使其支持事务。

现在,在本文的其余部分,我们将了解如何配置 WCF 服务以支持事务,以及如何在事务中调用 WCF 操作。

使用代码

理解两阶段提交

WCF 事务通过两阶段提交协议进行。两阶段提交协议是用于在分布式环境中启用事务的协议。该协议主要包含两个阶段:

  • 准备阶段:在此阶段,客户端应用程序执行 WCF 服务的操作。WCF 服务确定请求的操作是否会成功,并通知客户端。
  • 提交阶段:在提交阶段,客户端会检查从准备阶段收到的响应,如果所有响应都表明操作可以成功执行,则提交事务。如果任何一个操作的响应指示失败,则事务将被回滚。实际的服务端操作将在提交阶段进行。

现在从协议可以看出,WCF 服务必须将操作是否成功或失败的通知发送给客户端应用程序。这意味着单向操作永远无法支持事务。支持事务的操作必须遵循请求-响应模型(有关消息交换模式的详细信息,请参阅:WCF 中的消息交换模式和异步操作教程[^])

关于绑定的说明

由于服务以消息形式进行通信,因此底层消息规范在支持事务方面起着非常重要的作用。为了可能支持事务,需要使用 WS-AT(WS-AtomicTransaction) 协议。支持此协议的绑定是 wsHttpBinding 。因此,我们将使用此绑定来创建我们的支持事务的服务。

测试应用程序的描述

为了说明上述过程,假设我有两个账户持有人,一个人正试图向另一个人转账。从数据库的角度来看,此操作包含两个子操作,即:

  • 从第一个账户扣除指定金额。
  • 其次,向第二个账户存入所需金额。

现在从技术角度来看,如果第一个操作成功而第二个操作失败,结果将是第一个人的账户将被扣款,但第二个账户将不会被存入金额,即我们丢失了金额信息。反之,将在第二个账户中增加金额,而第一个账户甚至未被扣款。

因此,这里的底线是我们要么需要两者都成功,要么两者都失败。任何一个操作的成功都会导致不一致的结果,因此即使一个操作失败,我们也需要回滚在另一个操作中所做的操作。这正是事务有用武之地。

假设取款和存款操作是单独从服务中公开的。我们需要做的是启用此服务的事务,然后在事务中调用这两个方法。只有当两个操作都指示成功时,事务才会被提交。如果任何一个操作指示失败或抛出异常,事务将不会被提交。

创建服务

让我们创建一个简单的服务,其中包含一个 ServiceContract ,它公开用于扣款、存款和获取任何账户余额信息的 OperationContract

[ServiceContract]
public interface IService1
{
    [OperationContract]
    bool PerformCreditTransaction(string creditAccountID, double amount);

    [OperationContract]
    bool PerformDebitTransaction(string debitAccountID, double amount);

    [OperationContract]
    decimal GetAccountDetails(int id);
}

现在,我们希望 PerformCreditTransaction PerformDebitTransaction 操作在事务中运行。为此,我们需要对服务进行一些配置。与 OperationContract 相关的首要要求是设置 TransactionFlow 属性以及所需的 TransactionFlowOption TransactionFlowOption 有 3 种可能的值:

  • Mandatory: 这表示此函数只能在事务中调用。
  • Allowed: 这表示此操作可以在事务中调用,但并非强制。
  • NotAllowed: 这表示此操作不能在事务中调用。

现在,对于我们的两个操作,我们希望它们在事务中被强制调用,因此我们将它们指定为具有此选项。

[ServiceContract]
public interface IService1
{
    [OperationContract, TransactionFlow(TransactionFlowOption.Mandatory)]
    bool PerformCreditTransaction(string creditAccountID, double amount);

    [OperationContract, TransactionFlow(TransactionFlowOption.Mandatory)]
    bool PerformDebitTransaction(string debitAccountID, double amount);

    [OperationContract]
    decimal GetAccountDetails(int id);
}

现在,为了说明实现部分,我们将使用一个包含单个表数据库的小型应用程序。此表包含账户 ID 和账户中的金额。

示例数据库表如下:


用户界面如下:


现在,为了执行这些操作,我们将在服务实现中使用简单的 ADO.NET 代码。从服务实现的角度来看,重要的一点是服务实现还需要用 OperationBehavior 属性进行修饰/装饰,并将 TransactionScopeRequired 属性设置为 true 。现在让我们看一下服务操作的示例实现。

public class Service1 : IService1
{
    readonly string CONNECTION_STRING = ConfigurationManager.ConnectionStrings["SampleDbConnectionString1"].ConnectionString;

    [OperationBehavior(TransactionScopeRequired = true)]
    public bool PerformCreditTransaction(string creditAccountID, double amount)
    {
        bool creditResult = false;

        try
        {
            using (SqlConnection con = new SqlConnection(CONNECTION_STRING))
            {
                con.Open();

                // And now do a credit
                using (SqlCommand cmd = con.CreateCommand())
                {
                    cmd.CommandType = CommandType.Text;
                    cmd.CommandText = string.Format(
                        "update Account set Amount = Amount + {0} where ID = {1}",
                        amount, creditAccountID);

                    // Let us emulate some failure here to see the that transaction will not
                    // get committed
                    // return false;

                    creditResult = cmd.ExecuteNonQuery() == 1;
                }
            }
        }
        catch
        {
            throw new FaultException("Something went wring during credit");
        }
        return creditResult;
    }

    [OperationBehavior(TransactionScopeRequired = true)]
    public bool PerformDebitTransaction(string debitAccountID, double amount)
    {
        bool debitResult = false;

        try
        {
            using (SqlConnection con = new SqlConnection(CONNECTION_STRING))
            {
                con.Open();

                // Let us do a debit
                using (SqlCommand cmd = con.CreateCommand())
                {
                    cmd.CommandType = CommandType.Text;
                    cmd.CommandText = string.Format(
                        "update Account set Amount = Amount - {0} where ID = {1}",
                        amount, debitAccountID);

                    debitResult = cmd.ExecuteNonQuery() == 1;
                }
            }
        }
        catch
        {
            throw new FaultException("Something went wring during debit");
        }
        return debitResult;
    }

    public decimal GetAccountDetails(int id)
    {
        decimal? result = null;

        using (SqlConnection con = new SqlConnection(CONNECTION_STRING))
        {
            using (SqlCommand cmd = con.CreateCommand())
            {
                cmd.CommandType = CommandType.Text;
                cmd.CommandText = string.Format("select Amount from Account where ID = {0}", id);

                try
                {
                    con.Open();
                    result = cmd.ExecuteScalar() as decimal?;
                }
                catch (Exception ex)
                {
                    throw new FaultException(ex.Message);
                }
            }
        }

        if (result.HasValue)
        {
            return result.Value;
        }
        else
        {
            throw new FaultException("Unable to retrieve the amount");
        }
    }
}

注意:代码是为了详细说明支持事务的服务而编写的,不符合编码标准,即容易受到 SQL 注入攻击。不应将其视为可以投入生产的代码。这只是示例代码,还有很大的改进空间。

现在我们已经准备好了 ServiceContract 和服务的实现。现在让我们使用适当的绑定,即 wsHttpBinding 来使用此服务。此外,在服务配置中,我们需要指定此服务支持事务。这可以通过将 wsHttpBiding 的绑定配置的 transactionFlow 属性设置为 true 来完成。

注意:请参阅示例代码中的 web.config 了解如何执行此操作。

测试应用程序

从我们的测试应用程序中,我们将简单地在事务中调用函数(使用 TransactionScope )。我们将检查两个操作的返回值,如果两者都指示成功,我们将提交事务(通过调用 TransactionScope 对象上的 Complete 方法)。如果任何操作失败,我们将通过不调用 Complete 函数而让 TransactionScope 对象超出范围来回滚事务。

private void PerformTransaction(string creditAccountID, string debitAccountID, double amount)
{
    // they will be used to decide whether to commit or rollback the transaction
    bool debitResult = false;
    bool creditResult = false;

    try
    {
        using (TransactionScope ts = new TransactionScope())
        {
            using (ServiceReference1.Service1Client client = new ServiceReference1.Service1Client())
            {
                debitResult = client.PerformDebitTransaction(debitAccountID, amount);
                creditResult = client.PerformCreditTransaction(creditAccountID, amount);
            }

            if (debitResult && creditResult)
            {
                // To commit the transaction 
                ts.Complete();
            }
        }
    }
    catch
    {
        // the transaction scope will take care of rolling back
    }
}

代码目前可以正常工作,即两个方法都将返回 true,事务将被提交。要检查回滚操作,我们在服务的 PerformCreditTransaction 操作中有一个注释掉的简单 return false; 语句,取消注释此语句以检查事务是否未被提交。

注意:代码片段显示了相关代码的上下文。请查看示例代码以获得完整的理解。

现在我们有了一个支持事务的服务。在结束之前,让我们回顾一下使服务支持事务所需要的主要操作:

  1. 使用必需的 TransactionFlowOption 修饰 OperationContract
  2. 使用 TransactionScopeRequired 设置为 true 的 OperationBehavior 修饰操作实现。
  3. 使用 wsHttpBinding 来利用底层的 WS-AtomicTransaction 协议。
  4. wsHttpBinding 绑定配置的 transactionFlow 属性设置为 true

关注点

在本文中,我们讨论了如何创建支持事务的 WCF 服务。要完全理解本文,需要了解创建 WCF 服务、契约和绑定的知识,以及事务和 TransactionScope 的知识。希望这有所帮助。

历史

    2013 年 4 月 2 日:第一个版本。
© . All rights reserved.