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






4.51/5 (26投票s)
2004年5月7日
10分钟阅读

164055

1663
此演示向您展示了如何开发能够参与由 .NET Enterprise Services 协调的分布式事务的 .NET 组件。
引言
很久以前,在 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_ 表。
|
|
您可以使用 _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 - 事务演示”的示例。我感谢他所做的工作,更重要的是,感谢他为开启我的新概念思维所付出的耐心。