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

.NET Enterprise Services 上的 .NET 分布式事务:一个演示

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.51/5 (26投票s)

2004年5月7日

10分钟阅读

viewsIcon

164055

downloadIcon

1663

此演示向您展示了如何开发能够参与由 .NET Enterprise Services 协调的分布式事务的 .NET 组件。

Sample Image - KdotNET.gif

引言

很久以前,在 90 年代中期,我有一位聪明的同事,他喜欢通过示例来学习(和教学)。他能够为与计算机开发相关的任何主题创建清晰、简单、实用的示例。这些示例(他称之为“技术演示”)在所有同事中都很出名,有时被视为公司整体知识演进的里程碑。事实上,它们总是基于最新的微软技术开发的,通常是测试版。

我仍然清楚地记得他题为(别问我为什么)“K”的示例,其中他演示了 COM+ 分布式事务支持的使用,用 Visual C++ 6.0 和 Visual Basic 6.0 实现了一些组件,并让它们在 DTC 协调的事务中与两个 SQL Server 数据库协同工作。

当我在 .NET Beta 2 期间接触到“.NET Enterprise Services”主题时,我感受到了对一个清晰简单示例的缺失,就像那个事务演示“K”一样。但是——回想起我的导师几年前做的事情——我决定编写一个 .NET 版本。于是,“KdotNET”诞生了,现在它在这里向您展示实现 .NET 类以利用 COM+ 通过 .NET Enterprise Services 公开的事务服务有多么简单。

背景

KdotNET 的目标是展示如何开发能够与 .NET Enterprise Services 集成的事务类。乍一看,这似乎与 _.NET Framework SDK_ 中包含的众所周知的事务管理示例目标相同(在您的 .NET Framework 安装中检查 _...\SDK\v1.1\Samples\Technologies\ComponentServices\Transactions_ 文件夹,您就会找到它)。在那个示例中,他们展示了程序员在开发与单个数据源交互的单个类时如何利用 COM+ 事务支持(但您可以通过 _SqlTransaction_ 对象的 _Commit()_ 和 _Rollback()_ 方法获得类似的行为,不是吗?)。而在 _KdotNET_ 中,我实际上演示了更有趣的东西:Enterprise Services 如何管理跨多个数据源的 _分布式_ 事务,以及事务结果如何通过 COM+ 在多个事务类之间传播。

为了完成我们的演示,我们将考虑一个需要进行分布式事务的实际示例:一笔发生在两个银行账户之间的简单款项转账,这两个账户位于不同的银行,因此也在不同的数据库(这是一个非常经典的例子)。

我们当然会使用两个 SQL Server 2000 数据库。第一个名为 _Bank1_,它将位于 SQL Server “A”上,并将包含一个 _Accounts_ 表;该表存储 _Bank1_ 的账户。第二个数据库将是 _Bank2_(在服务器“B”上),并将包含另一个 _Accounts_ 表。

Bank1.Accounts 表
ID 描述 金额
1 Karl 100
2 Albert 200
3 Ricky 300
 
Bank2.Accounts 表
ID 描述 金额
1 Donald 1000
2 Mickey 2000
3 Minnie 3000

您可以使用 _CreatingDB.sql_ 脚本在 SQL Server 2000 上创建两个数据库。它会以默认选项(路径和文件大小)创建每个数据库;然后(在每个数据库上)创建 _Accounts_ 表,以 _ID_ 字段为主键,以 _Description_ 字段为 _UNIQUE_ 约束;然后,它会用一些示例数据填充每个 _Accounts_ 表。

还在 _Accounts_ 表上创建了一个 _INSERT_ / _UPDATE_ 触发器;其目标是检查单个账户的余额,以便表中不可能出现负余额:任何导致账户余额为负的 _INSERT_ 或 _UPDATE_ 操作都将被中止,回滚包装 _INSERT_ 或 _UPDATE_ 操作的隐式事务。

  CREATE TRIGGER Accounts_InsUpd 
  ON Accounts 
  FOR INSERT, UPDATE 
  AS
  DECLARE @remaining INT
  -- For simplicity, here the trigger manages one-row operations only
  IF (SELECT COUNT(*) FROM inserted) = 1
    BEGIN
    SELECT @remaining = Amount FROM inserted
    IF @remaining < 0
      BEGIN
      RAISERROR('Insufficient money.', 10, 1)
      ROLLBACK TRANSACTION
      END
    END

在接下来的段落中,我们将假设在 _Bank1_ 的一个账户和 _Bank2_ 的另一个账户之间进行款项转账;由于这些账户位于不同的服务器上,将启动一个分布式事务。我们将 _在_ 收账操作(例如,在服务器“B”上) _之前_ 执行扣款操作(例如,在服务器“A”上),以显示源账户负余额的触发方式以及它如何触发服务器“A”上的本地 _ROLLBACK_。我们将演示这个 _ROLLBACK_ 如何传播到整个分布式事务,导致已完成(或——最好是——“已准备好”但仍等待最终 _COMMIT_)的收账操作在服务器“B”上被实际 _撤销_,这得益于 Enterprise Services 公开的事务支持。

事务组件

为了简单起见,_KdotNET_ 中使用的事务组件被组织成非常小、基本级的类,每个类都专注于一项基本操作。

银行账户之间的款项转账由 _kmvbcls_ 类(程序集:_kmvb.dll_,命名空间:_kmvb_)管理。这个 VB.NET 类公开了 _transfer()_ 方法。

Public Function transfer(ByVal ConnString1 As String, _
       ByVal ConnString2 As String, _
       ByVal Account1 As String, ByVal Account2 As String, _
       ByVal AmountToTransfer As Integer) As Boolean

给定两个数据库的连接字符串、源账户和目标账户名称以及要转账的金额,此方法会实际执行转账,调用 _credit()_ 方法,然后调用 _charge()_ 方法。_charge()_ 方法由 _k1vbcls_ 类(程序集:_k1vb.dll_,命名空间:_k1vb_)公开。_credit()_ 方法由 _k2vbcls_ 类(程序集:_k2vb.dll_,命名空间:_k2vb_)公开。

以下是 _kmvbcls_ 类的代码

<TransactionAttribute(TransactionOption.Required)> _
Public Class kmvbcls
    Inherits ServicedComponent

  ' Transfer some money between two accounts (from Account1 to Account2),
  ' invoking methods from k1vbcls and k2vbcls classes
  ' Return: True if Okay, False otherwise
  Public Function transfer(ByVal ConnString1 As String, _
                    ByVal ConnString2 As String, _
                    ByVal Account1 As String, ByVal Account2 As String, _
                    ByVal AmountToTransfer As Integer) As Boolean
    Dim RetValue As Boolean
    Dim objCredit As New k2vb.k2vbcls
    Dim objCharge As New k1vb.k1vbcls

    Try
      objCredit.credit(ConnString2, Account2, AmountToTransfer)
      objCharge.charge(ConnString1, Account1, AmountToTransfer)
      RetValue = True
      ContextUtil.SetComplete()

    Catch exc As Exception
      RetValue = False
      ContextUtil.SetAbort()
      Throw New Exception("Error in kmvb:" & ControlChars.CrLf & exc.Message)

    Finally
      objCredit.Dispose()
      objCharge.Dispose()

    End Try
    Return RetValue

  End Function

End Class

三个类 _kmvbcls_、_k1vbcls_ 和 _k2vbcls_ 的实现包含一些使其可用于 Enterprise Services 上下文的特性:

  • 首先,它们都继承自 _System.EnterpriseServices.ServicedComponent_ 类;
  • 然后,包含每个类的程序集已被强命名(通过使用引用 _SN.EXE_ 工具生成的密钥文件的 _<Assembly: AssemblyKeyFileAttribute(...)>_ 属性);
  • 最后,每个类都用 _<TransactionAttribute(...)>_ 属性标记,指示 COM+ 需要为该类支持和管理事务。

以下是 _k1vbcls_ 和 _k2vbcls_ 类的代码

<TransactionAttribute(TransactionOption.Required)> _
Public Class k1vbcls
  Inherits ServicedComponent

  ' Charges on the specified database and account the specified amount
  Public Sub charge(ByVal ConnString As String, _
    ByVal Account As String, ByVal AmountToCharge As Integer)

    Dim strSQL As String
    strSQL = "UPDATE Accounts SET Amount = Amount - " _
             & AmountToCharge.ToString() & _
             " WHERE Description = '" & Account & "'"
    Dim HowManyRows As Integer      ' Number of modified rows
    Dim cnn As New SqlConnection(ConnString)
    Dim cmd As New SqlCommand(strSQL, cnn)

    Try
      cnn.Open()
      HowManyRows = cmd.ExecuteNonQuery()
      If HowManyRows = 1 Then
        ContextUtil.SetComplete()
        Exit Sub
      Else
        ' UPDATE failed
        ContextUtil.SetAbort()
        Throw New Exception("Invalid account or insufficient money.")
      End If

    Catch exc As Exception
      ContextUtil.SetAbort()
      Throw New Exception(exc.Message)

    Finally
      cmd.Dispose()
      cnn.Close()
    End Try

  End Sub

End Class


<TransactionAttribute(TransactionOption.Required)> _
Public Class k2vbcls
  Inherits ServicedComponent

  ' Credits to the specified database and account the specified amount
  Public Sub credit(ByVal ConnString As String, _
    ByVal Account As String, ByVal AmountToCredit As Integer)

    Dim strSQL As String
    strSQL = "UPDATE Accounts SET Amount = Amount + " _
             & AmountToCredit.ToString() & _
             " WHERE Description = '" & Account & "'"
    Dim HowManyRows As Integer      ' Number of modified rows
    Dim cnn As New SqlConnection(ConnString)
    Dim cmd As New SqlCommand(strSQL, cnn)

    Try
      cnn.Open()
      HowManyRows = cmd.ExecuteNonQuery()
      If HowManyRows = 1 Then
        ContextUtil.SetComplete()
        Exit Sub
      Else
        ' UPDATE failed
        ContextUtil.SetAbort()
        Throw New Exception("Invalid account.")
      End If

    Catch exc As Exception
      ContextUtil.SetAbort()
      Throw New Exception(exc.Message)

    Finally
      cmd.Dispose()
      cnn.Close()
    End Try

  End Sub

End Class

为了让 COM+ 了解事务的单个部分的结果,这些组件利用 _System.EnterpriseServices.ContextUtil_ 类中的 _SetComplete()_ 和 _SetAbort()_ 方法。 _NET Framework_ 通过使用 _System.EnterpriseServices.AutoCompleteAttribute_ 属性(在上面提到的 SDK 示例中也显示了)提供了这种 _手动_ 管理部分事务结果的替代方法。此属性指示 COM+,给定方法

  • 如果正常完成,将对事务提交投票(实际上,隐式调用 _SetComplete()_),或
  • 如果在执行期间抛出任何异常,将投票回滚事务(实际上,隐式调用 _SetAbort()_)。

_KdotNET_ 展示了两种实现 _ServicedComponent_ 类事务投票的替代方法:

  • 前面描述的 _kmvbcls_、_k1vbcls_ 和 _k2vbcls_ 三个类中展示了 _SetComplete()_ / _SetAbort()_ 方法;
  • 为 C# 爱好者提供了 _AutoCompleteAttribute_ 方法,它显示在 _kmcscls_、_k1cscls_ 和 _k2cscls_ 这三个类中,它们分别是 _kmvbcls_、_k1vbcls_ 和 _k2vbcls_ 的 C# 等价类。

如何运行演示

我们刚刚开发的 _KdotNET_ 组件已准备好使用,无需在 COM+ 中进行任何注册。这是因为包含我们类的程序集是强命名的,并且 .NET Framework Enterprise Services 能够为继承自 _ServicedComponent_ 类的类提供自动注册。

因此,我们只需要一个 _客户端_ 来实例化我们的类并调用它们的方法;特别是,我们的最终目标将是使用 _kmvbcls_(或 _kmcscls_)类,调用其 _transfer()_ 方法来查看分布式事务是否真的有效,向我们展示我们期望的“_全有或全无_”行为。为了完成这种测试,我提供了 _CompTest_(代表“Component Tester”),一个小的 Windows Forms 客户端,我的导师会称之为“_pulsantiera_”(意大利语,我们用来表示一个充满无用按钮的表单)。

正如您所见(通过其自描述的用户界面),这个简单的客户端允许您测试我们组件的每个单独方法(以 VB.NET 或 C# 版本):通过点击 _charge_ 或 _credit_ 按钮,您可以分别测试将指定金额充值或扣除到指定银行账户的基本操作;显然,只有通过点击 _transfer_ 按钮,您才能真正启动银行账户之间的款项转账,从而启动分布式事务。请记住,将“Money to transfer”文本框设置为负值,您可以反转转账方向(从 _Bank2_ 到 _Bank1_ 而不是从 _Bank1_ 到 _Bank2_)。

在进行此测试活动期间,当然,您需要验证 _Bank1_ 和 _Bank2_ 的 _Accounts_ 表中数据的实际状态。为此,您可以使用 _BankMonitor_,一个我开发的实用程序,用于简化银行账户监控任务。这是另一个 Windows Forms 应用程序,它以指定的时间间隔查询 _Accounts_ 表并显示其内容。

_BankMonitor_ 在不考虑数据库锁的情况下读取 _Accounts_ 表(即:使用“未提交读”事务隔离级别)。这种行为允许您查看“脏读”,并调查分布式事务执行期间的瞬时、未提交状态。因此,您可以看到,在银行账户资金不足的情况下,分布式事务会有效地回滚。为了使这一点更加明显:修改 _transfer()_ 方法,在信用和扣款操作之间添加 5 秒的等待时间(如下所示),然后尝试转账金额大于捐赠者可用金额。

  ...
  objCredit.credit(ConnString2, Account2, AmountToTransfer)
  System.Threading.Thread.Sleep(5000)
  objCharge.charge(ConnString1, Account1, AmountToTransfer)
  ...

在此款项转账过程中(如果刷新率小于 5 秒,比如 3 秒),如果您查看 _BankMonitor_ 窗口,您会看到目标银行账户金额暂时增加(当然,这是未提交的状态);然后,一旦扣款操作失败,它就会减少,导致 _k1vb_(或 _k1cs_)组件投票事务失败(调用显式或隐式 _SetAbort()_),从而导致整个分布式事务的 _ROLLBACK_。

从 ASP.NET 网页调用服务组件

测试我们的事务组件的另一种方法是在 ASP.NET Web Form 中进行。您可以准备一个 ASP.NET 项目,将 _kmvb.dll_、_k1vb.dll_ 和 _k2vb.dll_(或它们的 C# 等价物)作为私有程序集引用,并像 _CompTest_ 一样调用 _transfer()_ 方法。运行该 Web 项目并使用 _BankMonitor_ 监控其活动,您将看到(惊喜!?)一切都像 Windows 应用程序客户端一样工作。

但是,如果您利用使 ASPX 页面本身 _事务化_ 的能力,通过将 _Transaction_ 属性添加到 _@Page_ 指令,您还可以提供一个更不寻常的 Web 示例:

<%@ Page Transaction="Required" Language="vb" Codebehind="WebForm1.aspx.vb" ... %>

设置此属性后,页面本身会将事务上下文传播给调用的服务组件,因此无需使用 _kmvbcls_ 类来协调分布式事务。此方法显示在 _WebCompTest_(Web Component Tester)项目中,这是一个包含此 Web Form 的简单 ASP.NET Web 应用程序。

“Transfer”按钮的 _Click_ 事件的代码只是实例化 _k1vbcls_ 和 _k2vbcls_ 类,并调用它们的方法。

  Private Sub cmdTransfer_Click(...) Handles cmdTransfer.Click
    Dim objCredit As New k2vb.k2vbcls
    Dim objCharge As New k1vb.k1vbcls

    Try
      objCredit.credit(txtConn2.Text, _
        txtAccount2.Text, Convert.ToInt32(txtAmount.Text))
      objCharge.charge(txtConn1.Text, _
        txtAccount1.Text, Convert.ToInt32(txtAmount.Text))
      lblResult.Text = "Success (" & System.DateTime.Now().ToString() & ")"

    Catch exc As Exception
      lblResult.Text = "Failure (" & System.DateTime.Now().ToString() & ")" & _
          "<BR>" & exc.Message

    Finally
      objCredit.Dispose()
      objCharge.Dispose()

    End Try
  End Sub

当然,如果您从 _@Page_ 指令中删除 _Transaction_ 属性(并且避免使用 _kmvbcls_ 类),ASPX 页面将停止与调用的组件共享其事务上下文:因此,_objCredit_ 和 _objCharge_ 对象将在自己的事务中运行,并且不知道其他对象执行的操作的结果。不会启动分布式事务,系统将允许 Karl 在任何情况下(即使他的银行账户余额不足)都能给予资金:_charge()_ 方法会失败, _而不会_ 导致先前执行的 _credit()_ 操作的 _撤销_。在这种情况下,结果是 Karl 不劳而获。哦,幸运的 Karl!

致谢

_KdotNET_ 的想法源于几年前由 Carlo Randone 在 Windows DNA 架构上开发的题为“K - 事务演示”的示例。我感谢他所做的工作,更重要的是,感谢他为开启我的新概念思维所付出的耐心。

© . All rights reserved.