使用 .NET 构建分布式应用程序

Priya Dhawan

Microsoft Developer Network

2002 年 11 月

摘要:本文描述了如何在 Microsoft .NET 应用程序中运行本地事务和分布式事务。(14 页打印页)

*
本页内容
简介 简介
本地事务和分布式事务 本地事务和分布式事务
数据库事务 数据库事务
手动事务 手动事务
自动事务 自动事务
小结 小结


简介

事务是作为单个工作单元执行的一系列操作。通过将一组相关操作绑定在一个事务中,虽然仍然可能发生任何错误,但您可以确保系统的一致性和可靠性。为了使事务成功,必须成功完成该事务中的所有操作。

事务有一个开头和一个结尾,它们指定了事务的边界,事务在其边界之内可以跨越进程和计算机。事务边界内的所有资源都参与同一个事务。要维护事务边界内资源间的一致性,事务必须具备 ACID 属性,即原子性、一致性、隔离性和持续性。有关事务处理基本原理的详细信息,请参见 Microsoft .NET Framework SDK 中的 Processing Transactions

本文将主要讨论如何在 Microsoft .NET 应用程序中运行本地事务和分布式事务。

返回页首返回页首


本地事务和分布式事务

本地事务是其范围为单个可识别事务的数据资源的事务(例如,Microsoft? SQL Server? 数据库或 MSMQ 消息队列)。例如,当单个数据库系统拥有事务中涉及的所有数据时,就可以遵循 ACID 规则。在 SQL Server 的情况下,由内部事务管理器来实现事务的提交和回滚操作。

分布式事务可以跨越不同种类的可识别事务的数据资源,并且可以包括多种操作(例如,从 SQL 数据库检索数据、从 Message Queue Server 读取消息以及向其他数据库进行写入)。通过利用跨若干个数据资源来协调提交和中止操作以及恢复的软件,可以简化分布式事务的编程。Microsoft Distributed Transaction Coordinator (DTC) 就是一种这样的技术。它采用一个二阶段的提交协议,该协议可确保事务结果在事务中涉及的所有数据资源之间保持一致。DTC 只支持已实现了用于事务管理的兼容接口的应用程序。这些应用程序被称为资源管理器(有关此主题的详细信息,请参见 .NET Framework Developer’s Guide 中的 Distributed Transactions),目前存在许多这样的应用程序,包括 MSMQ、Microsoft SQL Server、Oracle、Sybase 等等。

返回页首返回页首


数据库事务

如果调用一个在 BEGIN TRANSACTION 和 COMMIT/ROLLBACK TRANSACTION 语句中封装了所需操作的存储过程,您就可以在到数据库服务器的单个往返行程中运行事务,从而实现最佳性能。数据库事务还支持嵌套事务,这意味着您可以从一个活动事务中启动一个新事务。

在下面的代码片断中,BEGIN TRANSACTION 语句开始了一个新事务。可以通过使用 COMMIT TRANSACTION 语句将更改提交到数据库来结束事务,或者,在发生任何错误的情况下,通过使用 ROLLBACK TRANSACTION 语句将所有更改撤消来结束事务:

CREATE PROCEDURE Proc1
???…
AS
   -- Begin the transaction
   BEGIN TRANSACTION
   -- Do transaction operations
   ???…
   -- Check for any Error
   If @@Error <> 0
      -- Rollback the Transaction
      ROLLBACK TRANSACTION
   ???…
   -- Commit the Transaction
   COMMIT TRANSACTION

下面的存储过程接受作为输入参数的定单的 XML 表示形式。为了在 Orders 和 OrderDetails 表中进行适当的插入,该存储过程使用 sp_xmlpreparedocument 系统存储过程来加载和分析 XML。正如您在代码中所看到的,存储过程在一个显式事务中封装了所有操作,因此,如果其中的任何操作执行失败,将回滚所做的所有更改。

注意,该过程将 XACT_ABORT 设置为 ON,此设置的作用是,如果其中的任何语句无法完成,SQL 服务器将自动回滚事务。

CREATE PROCEDURE InsertOrder
@Order  NVARCHAR(4000) = NULL
, @OrderId int Output
AS
   SET NOCOUNT ON
   DECLARE @hDoc INT
   DECLARE @PKId  INT
   -- Specify that the SQL Server automatically rolls back the current
   -- transaction if a Transact-SQL statement raises a run-time error.
   SET XACT_ABORT ON
   -- Begin the transaction
   BEGIN TRANSACTION
   -- Load and Parse the incoming XML represeting an Order into an
   -- XMLDocument
   EXEC sp_xml_preparedocument @hDoc OUTPUT, @Order
   -- Select order header from the Order node in the XMLDocument and
   -- insert it into the Orders table
   INSERT Orders(CustomerId,
                  OrderDate,
                  ShipToName,
                  ShipToAddressId,
                  OrderStatus)
SELECT CustomerId, CONVERT(DateTime,OrderDate), ShipToName,
 ShipToAddressId, OrderStatus
   FROM OPENXML(@hDoc, '/NewDataSet/Orders')
   WITH ( CustomerId int 'CustomerId',
          OrderDate nvarchar(23) 'OrderDate',
          ShipToName nvarchar(40) 'ShipToName',
          ShipToAddressId int 'ShipToAddressId',
          OrderStatus  int 'OrderStatus')
   -- Select the OrderId of the Order just inserted into the Orders table
   -- to use it while inserting order details
   SELECT @PKId = @@IDENTITY
   -- Select order details from the Details node in the XMLDocument and
   -- insert them into the OrderDetails table
   INSERT OrderDetails (OrderId,
                       ItemId,
                       UnitPrice,
                       Quantity)
   SELECT @PKId as OrderId, ItemId, UnitPrice, Quantity
   FROM OPENXML(@hDoc, '/NewDataSet/Details')
   WITH (ItemId int 'ItemId',
         UnitPrice money 'UnitPrice',
         Quantity int 'Quantity')
   -- Set the Output parameter
   Select @OrderId = @PKId
   -- Commit the transaction
   COMMIT TRANSACTION
   EXEC sp_xml_removedocument @hDoc
   RETURN 0
GO

虽然这种做法提供了良好的性能,但是,您仍然需要用 Transact SQL 编写代码,这可不象用诸如兼容 .NET 的语言编写代码那么简单。

返回页首返回页首


手动事务

利用手动事务,您可以使用开始和结束事务的显式指令来显式控制事务边界。此模式还支持允许您从活动事务中开始一个新事务的嵌套事务。但是,应用此控制会给您增加一种额外负担,您需要向事务边界登记数据资源并对这些资源进行协调。由于对分布式事务没有任何内置的支持,因此,如果您选择以手动方式来控制分布式事务,将承担许多责任;您需要控制每个连接和资源登记,并通过提供实现来保持事务的 ACID 属性。

ADO.NET 手动事务

这两种 Microsoft ADO.NET 数据提供程序通过提供创建到数据存储区的连接、开始一个事务、提交或中止事务以及最后关闭连接的一组对象来启用手动事务。我们将以 ADO.NET SQL 托管提供程序为例来进行说明。

要在单个事务中执行操作,您需要创建 SQLTransaction 对象、使用 SQLConnection 对象开始事务、确保在事务内进行数据库交互以及提交或中止事务。SQLTransaction 对象提供了多种方法和属性来控制事务。如果事务中的每个操作都已经成功完成,可以使用 “提交” 方法将所做的更改提交到数据库。使用 SQLTransaction 对象的 “回滚” 方法可以回滚更改。

注意 "命令"; 对象的 Transaction 属性必须设置为一个已经开始的事务,这样,它才能在该事务中执行。

Visual Basic .NET

Dim conn as SQLConnection
Dim cmd as SQLCommand
Dim txn As SQLTransaction
conn = New SQLConnection("ConnString")
cmd = New SQLCommand
' Open a conection
conn.Open()
' Begin a transaction
txn = conn.BeginTransaction()
' Set the Transaction in which the command executes
cmd.Transaction = Txn
???…

Visual C# .NET

SQLConnection Conn = New SQLConnection("ConnString");
SQLCommand Cmd = New SQLCommand;
// Open a connection
Conn.Open();
// Begin a transaction
SQLTransaction Txn = Conn.BeginTransaction();
// Set the Transaction in which the command executes
Cmd.Transaction = Txn;
???…

在下面的示例中,我们在一个事务的边界内执行两个 SQL 命令。第一个命令将定单的定单标题插入 Orders 表并返回新插入的定单的 OrderId,第二个命令使用该 OrderId 将同一个定单的详细信息插入 OrderDetails 表中。如果这两个命令中的任何一个未能成功执行,该事务将被中止,从而避免将行添加到数据库中。

Visual Basic .NET

Dim conn As SqlConnection
Dim cmd As SqlCommand
Dim tran As SqlTransaction
' Create a New Connection
conn = New SqlConnection("ConnString")
' Open the Connection
conn.Open()
' Create a new Command object
cmd = New SqlCommand()
' Create a new Transaction
tran = conn.BeginTransaction
' Set the Transaction within which the Commands execute
cmd.Transaction = tran
Try
  ' Insert the Order header
  ' Set the Command properties
   With cmd
      .CommandType = CommandType.StoredProcedure
      .CommandText = "InsertOrderHeader"
      .Connection = conn
      ' Add input and output parameters
      .Parameters.Add("@CustomerId", SqlDbType.Int)
      .Parameters("@CustomerId").Direction = ParameterDirection.Input
      ???…
      ' Set the parameter values
      .Parameters("@CustomerId").Value = 1
      ???…
      ' Execute the command
      .ExecuteNonQuery()
      ' Get the OrderId of the newly selected order header
      OrderId = .Parameters("@OrderId").Value
      ' Clear the parameters for the next command
      .Parameters.clear()
   End With
   ' Insert the Order Details
   ' Set Command properties
   With cmd
      .CommandType = CommandType.StoredProcedure
      .CommandText = "InsertOrderDetail"
      .Connection = conn
      ' Add parameters
      .Parameters.Add("@OrderId", SqlDbType.Int)
      .Parameters("@OrderId").SourceColumn = "OrderId"
      .Parameters("@OrderId").Direction = ParameterDirection.Input
      ???…
      ' Set the parameter values
      .Parameters("@OrderId").Value = OrderId
      .Parameters("@ItemId").Value = 100
      ???…
      ' Execute the command
      .ExecuteNonQuery()
      ' Repeat the above few lines for each order detail
   End With
   ' Commit the Transaction
   tran.Commit()
Catch
   ' Rollback the Transaction
   tran.Rollback()
Finally
   ' Cleanup Code
   ' Close the Connection
   conn.Close()
End Try

正如您在上面的代码片断中所看到的那样,这两个命令是作为单个事务的一部分执行的。如果其中任何一个命令失败,该事务将中止,并回滚对数据库所做的任何更改。通过在 try/catch/finally 程序块中封装代码,可确保事务正确执行;当两个命令都已成功执行后,才紧随 try 程序块之后提交该事务。在 catch 程序块中捕获所引发的任何异常,该程序块可中止事务以撤消在事务中所做的更改。

通过 ADO.NET 对象控制事务来进行锁定不如在存储过程中使用显式事务进行的锁定那么有效。原因是,除了开始和结束事务的行程,ADO.NET 手动事务到 DBMS 的往返行程至少和事务中要执行的操作一样多。当调用在 ADO.NET 代码和数据库服务器之间来回发送时,您始终控制着锁定。

MSMQ 手动事务

.NET Framework 以两种不同的方式支持 MSMQ 事务:通过允许多个消息作为事务的一部分发送或接收而手动(内部)支持;通过参与 Distributed Transaction Coordinator (DTC) 事务而自动(外部)支持。

MSMQ 手动事务是通过 MessageQueueTransaction 类来支持的,并且完全在 MSMQ 引擎内处理。有关详细信息,请参见 Duncan Mackenzie 的文章 Reliable Messaging with MSMQ and .NET

返回页首返回页首


自动事务

.NET Framework 依靠 MTS/COM+ 服务来支持自动事务。COM+ 使用 Microsoft Distributed Transaction Coordinator (DTC) 作为事务管理器和事务协调器在分布式环境中运行事务。这样可使 .NET 应用程序运行跨多个资源结合不同操作(例如,将定单插入 SQL Server 数据库、将消息写入 Microsoft 消息队列 (MSMQ) 队列、发送电子邮件以及从 Oracle 数据库检索数据)的事务。

通过提供基于声明性事务的编程模型,COM+ 使应用程序可以很容易地运行跨不同种类的资源的事务。这种做法的缺点是,由于存在 DTC 和 COM 互操作性开销,导致性能降低,而且不支持嵌套事务。

ASP.NET 页、Web Service 方法和 .NET 类通过设置声明性事务属性都可以标记为事务性。

ASP.NET

<@ Page Transaction="Required">

ASP.NET Web 服务

<%@ WebService Language="VB" Class="Class1" %>
<%@ assembly name="System.EnterpriseServices" %>
???…
Public Class Class1
   Inherits WebService
   <WebMethod(TransactionOption := TransactionOption.RequiresNew)> _
Public Function Method1()
???…

要参与自动事务,.NET 类必须是从 System.EnterpriseServices.ServicedComponent 类继承的,这可使得该 .NET 类能够在 COM+ 内运行。在这个过程中,要将 COM+ 与 DTC 进行交互以创建一个分布式事务,也要登记后台的所有资源连接。您还需要对该类设置声明性事务属性以确定其事务性行为。

Visual Basic .NET

<Transaction(TransactionOption.Required)> Public Class Class1
    Inherits ServicedComponent

Visual C# .NET

[Transaction(TransactionOption.Required)]
public class Class1 : ServicedComponent {
???…
}

类的事务属性可以设置为以下任何选项:

禁用— 指示该对象从不在 COM+ 事务中创建。该对象可以直接使用 DTC 来获得事务性支持。

NotSupported— 指示该对象从不在事务中创建。

支持— 指示该对象在其创建者的事务的上下文中运行。如果该对象本身是根对象,或者其创建者不在事务中运行,则该对象将在不使用事务的情况下创建。

必选— 指示该对象在其创建者的事务的上下文中运行。如果该对象本身是根对象,或者其创建者不在事务中运行,则该对象将使用一个新事务来创建。

RequiresNew— 指示该对象需要一个事务,并且该对象使用新事务来创建。

下面的代码显示了配置为在 COM+ 中运行、将程序集属性设置为配置 COM+ 应用程序属性的 .NET 类。

Visual Basic .NET

Imports System
Imports System.Runtime.CompilerServices
Imports System.EnterpriseServices
Imports System.Reflection
'Registration details.
'COM+ application name as it appears in the COM+ catalog
<Assembly: ApplicationName("Class1")>
'Strong name for assembly
<Assembly: AssemblyKeyFileAttribute("class1.snk")>
<Assembly: ApplicationActivation(ActivationOption.Server)>
<Transaction(TransactionOption.Required)> Public Class Class1
    Inherits ServicedComponent
    Public Sub Example1()
      ???…
    End Sub
End Class

Visual C# .NET

using System;
using System.Runtime.CompilerServices;
using System.EnterpriseServices;
using System.Reflection;
//Registration details.
//COM+ application name as it appears in the COM+ catalog
[Assembly: ApplicationName("Class1")]
'Strong name for assembly
[Assembly: AssemblyKeyFileAttribute("class1.snk")]
[Assembly: ApplicationActivation(ActivationOption.Server)]
[Transaction(TransactionOption.Required)]
public class Class1 : ServicedComponent {
       [AutoComplete]
    public void Example1()
{
        ???…
    }
}

指定要安装该程序集的组件的 COM+ 应用程序的名称。 指定 COM+ 应用程序是否为服务器应用程序或库应用程序。指定 ApplicationActivation(ActivationOption.Server)时,必须使用 gacutil 命令行工具 (GacUtil.exe) 将程序集安装到全局程序集缓存 (GAC)。

您可以使用 Regsvcs.exe 命令行工具将程序集转换为类型库,并将类型库注册和安装到指定的 COM+ 应用程序中。该工具还可用来配置您已经用编程方式添加到程序集中的属性。例如,如果在程序集中指定 ApplicationActivation(ActivationOption.Server)(ActivationOption.Server),该工具将创建一个服务器应用程序。如果在未使用 COM+ 来安装程序集的情况下调用程序集,运行时将创建和注册一个类型库,并使用 COM+ 来安装该库。您可以在组件服务管理单元中看到和配置为程序集创建的 COM+ 应用程序。

在 .NET Framework Developer’s Guide 的 Writing Serviced Components中详细介绍了服务组件的创建、注册和使用过程。

下面的代码片断显示了配置为在 COM+ 下运行的事务性类,它在事务边界内执行两个 SQL 命令。第一个命令将定单的定单标题插入 Orders 表并返回新插入的定单的 OrderId,第二个命令使用该 OrderId 将同一个定单的详细信息插入 OrderDetails 表中。如果这两个命令中的任何一个未能成功执行,该事务将被中止,从而避免将行添加到数据库中。

Visual Basic .NET

<Transaction(TransactionOption.Required)> Public Class Class1
    Inherits ServicedComponent
 Public Sub Example1()
        ???…
        Try
            ' Create a New Connection
            conn = New SqlConnection("ConnString")
            ' Open the Connection
            conn.Open()
            ' Create a new Command object
            cmd = New SqlCommand()
            ' Insert the Order header
            ' Set the Command properties
            With cmd1
                .CommandType = CommandType.StoredProcedure
                .CommandText = "InsertOrderHeader"
                .Connection = conn
                ' Add input and output parameters
                .Parameters.Add("@CustomerId", SqlDbType.Int)
                ???…
               .ExecuteNonQuery()
               ' Clear the parameters for the next command
               .Parameters.clear()
            End With
            ' Insert all the Order details
            ' Set Command properties
            With cmd
                .CommandType = CommandType.StoredProcedure
                .CommandText = "InsertOrderDetail"
                .Connection = conn
                ' Add parameters
                .Parameters.Add("@OrderId", SqlDbType.Int)
                ???…
                ' Execute the command
               .ExecuteNonQuery()
                ' Repeat the above few lines for each order
                     detail
            End With
            ' Commit the Transaction
            ContextUtil.SetComplete()
        Catch
            ' Rollback the Transaction
            ContextUtil.SetAbort()
        Finally
            ' Cleanup Code
        End Try
    End Sub

通过使用System.EnterpriseServices.ContextUtil 类,可以获得有关 COM+ 对象上下文的信息。它提供SetComplete SetAbort 方法,以便分别显式提交和回滚事务。正如您预想的那样,当所有操作已成功执行后,紧随 try 程序块的最后调用 ContextUtil.SetComplete 方法来提交事务。所引发的任何异常将在 catch 程序块中被捕获,该程序块使用ContextUtil.SetAbort 中止事务。

您还可以使用System.EnterpriseServices.AutoComplete 属性类来让服务组件自动选择是提交事务还是中止事务。如果方法调用成功返回,组件将倾向于选择提交事务。如果方法调用引发异常,事务会自动中止;您无需显式调用 ContextUtilSetAbort。要使用此功能,应在类方法之前插入 <AutoComplete> 属性:

Visual Basic .NET

<Transaction(TransactionOption.Required)> Public Class Class1
    Inherits ServicedComponent
    <AutoComplete()> Public Sub Example1()
      ???…
    End Sub
End Class

Visual C# .NET

[Transaction(TransactionOption.Required)]
public class Class1 : ServicedComponent {
       [AutoComplete]
    public void Example1()
{
        ???…
    }
}

利用 属性,您不必显式提交或回滚事务,从而为进行事务性编程提供了最简单的方法。您获得的功能与前面的示例中获得的功能完全一样。在前面的示例中,我们在 catch 程序块中显式调用ContextUtil.SetAbort 来中止事务。这种方法的缺点是,事务的使用非常不明显,而且,有可能在以后维护代码时被忘记。另外,如果您希望在事务失败时发出一条用户友好的消息,也无法实现。在这类情况下,您应该显式捕获任何异常,调用ContextUtil.SetAbort,然后发出自定义消息。

在需要事务跨 MSMQ 和其他可识别事务的资源(例如,SQL Server 数据库)运行的系统中,只能使用 DTC 或 COM+ 事务,除此之外没有其他选择。DTC 协调参与分布式事务的所有资源管理器,也管理与事务相关的操作。有关跨越 MSMQ 和 SQL Server 的分布式事务的示例,请参见 Duncan Mackenzie 的文章 Reliable Messaging with MSMQ and .NET

返回页首返回页首


小结

每一种事务方法都是应用程序性能和代码可维护性的折衷。运行在存储过程中实现的数据库事务可提供最佳性能,因为它只需要到数据库的单个往返行程。另外,这种方法还提供了显式控制事务边界的灵活性。虽然它提供了良好的性能和灵活性,但您需要用 Transact SQL 来编写代码,这就不如用 .NET 来编写代码那么简单。

使用 ADO.NET 事务对象的手动事务很易于编写代码,并实现了用显式指令开始和结束事务以控制事务边界的灵活性。但是,为获得这种简易性和灵活性,需要一些完成事务所需的到数据库的额外往返行程,这导致了性能降低。

如果事务跨越多个可识别事务的管理器(可能包括 SQL Server 数据库、MSMQ 消息队列等等),自动事务将是唯一的选择。这种方法大大简化了应用程序设计,减少了编码需求。不过,由于 COM+ 服务执行所有协调工作,可能有一些额外的开销。

转到原英文页面


评论

该日志第一篇评论

发表评论

评论也有版权!