Indigo 事务性 Web 服务(使用 Microsoft WinFX CTP March 2005)
在 Indigo 中实现 WS-AtomicTransaction。
引言
Microsoft 发布了首个包含 Indigo 的 WinFx CTP,其中包含了备受期待的分布式事务 (WS-Atomic Transaction) 支持。在本文中,我将编写一些非常简单的 C# 代码来阐述 Indigo 中的分布式事务概念。运行示例应用程序的唯一要求是下载并安装 WinFx CTP 到 SP2 的 Windows XP 上,无需 Visual Studio .NET 2005 Beta。我希望这将帮助您理解事务编程的语义以及如何利用事务模型来处理常见的业务问题。
事务代码
大多数开发人员都熟悉数据库事务代码,如下面的代码段所示。
void DoLocalDBTransaction(int OrderID)
{
System.Configuration.AppSettingsReader reader =
new System.Configuration.AppSettingsReader();
string cnstr =
reader.GetValue("ConnectionString",typeof(string)).ToString();
SqlConnection cn = new SqlConnection(cnstr);
cn.Open((new SqlCommand("update orders set orderDate='" +
DateTime.Now.ToString() + "' where OrderID=" + OrderID.ToString());
cn.ExecuteNonQuery();
cn.Close();
}
这种类型的事务代码可以在本地可执行文件以及远程 Web 服务的 WebMethod 中找到。当多次调用此函数时,会出现有趣的场景:如果第一次调用成功而最后一次调用失败,导致数据库部分更新怎么办?.NET 1.1 框架的标准解决方案是使用 COM+ 或 ADO.NET 事务支持进行回滚。但如果事务之一是通过调用远程 Web 服务执行的,那么回滚将不会发生,因为 COM+ 和 ADO.NET 无法在 SOAP/HTTP 上运行。这时,.NET Framework 2.0 的 System.Transaction
和 Indigo 的 System.ServiceModel
事务支持就派上用场了。
分布式事务的基本模型
WS-Atomic Transaction 是一个非常简单但优雅的理念,如下图所示。
此图说明了 WS-Atomic Transaction (也称为 WS-AT) 的几个关键要素。
- 每个 WS-AT 只有一个事务协调器。其唯一目的是整体完成或回滚事务。
- 每个事务参与者都必须向事务协调器 (MSDTC) 注册,包括本地数据库事务和 Web 服务事务上下文 (DB 或非 DB)。当协调器上下文跨越边界流动时,Indigo 消息总线也会联系协调器。
这只是 WS-AT 事务规范的一个高级简化模型。现在让我们来看一些代码。
WS-AT 协调器
协调器可以用 System.Transaction.TransactionScope
表示。
using (TransactionScope scope = new TransactionScope())
{
DoLocalDBTransaction(10248);
DoWSDBTransaction(10249);
DoWSNonDBTransaction(CustomerID);
scope.Complete();
}
我们在介绍中已经看到了本地数据库事务的代码。以下是其他两个函数的代码。
void DoWSDBTransaction(int OrderID)
{
EndpointAddress address =
new EndpointAddress("https:///TransactionalWS/Service.svc");
WSProfileBinding binding = new WSProfileBinding();
binding.FlowTransactions = ContextFlowOption.Required;
TransactionalWebServiceProxy proxy =
new TransactionalWebServiceProxy(address, binding);
proxy.NewInnerProxy.DoDBTransaction(OrderID);
}
void DoWSNonDBTransaction(int CustomerID)
{
EndpointAddress address =
new EndpointAddress("https:///TransactionalWS/Service.svc");
WSProfileBinding binding = new WSProfileBinding();
binding.FlowTransactions = ContextFlowOption.Required;
TransactionalWebServiceProxy proxy =
new TransactionalWebServiceProxy(address, binding);
proxy.NewInnerProxy.DoNonDBTransaction(CustomerID);
}
就像 ASMX Web 服务模型一样,Indigo 仍然使用代理/存根网络通信架构,并且这是由 svcutil.exe 工具生成的 Indigo 代理代码,然后手动修改。
class TransactionalWebServiceProxy :
ProxyBase<ITransactionalWebService><ITRANSACTIONALWEBSERVICE>
{
public TransactionalWebServiceProxy(
EndpointAddress address, WSProfileBinding binding)
: base(address, binding)
{
}
public ITransactionalWebService NewInnerProxy
{
get { return InnerProxy; }
}
}
其中 ITransactionalWebService
是 Indigo Web 服务契约接口。
public interface ITransactionalWebService
{
[OperationContract()]
void DoDBTransaction( int OrderID);
[OperationContract()]
void DoNonDBTransaction(int CustomerID);
}
这些是“上线 Indigo 消息总线”的样板代码,类似于在 VS.NET 2003 中添加 Web 引用。本质上,我们通过建立代理并通过它流动上下文来构建 Indigo 通信。关于 Indigo 消息总线的网络通信还有一些有趣的细节,现在让我们来看一下。
Indigo ABC
A 代表地址,B 代表绑定,C 代表契约。要建立 Indigo 通信,我们必须同时处理通信两端的 ABC。
对于事务协调器所在的物理机 1 上的 Indigo ABC,我们在上面已经看到了以下代码。
EndpointAddress address =
new EndpointAddress("https:///TransactionalWS/Service.svc");
WSProfileBinding binding = new WSProfileBinding();
binding.FlowTransactions = ContextFlowOption.Required;
TransactionalWebServiceProxy proxy =
new TransactionalWebServiceProxy(address, binding);
proxy.NewInnerProxy.DoNonDBTransaction(CustomerID);
这里的 A 是 https:///TransactionalWS/Service.svc;B 是 wsProfileBinding
,这是 Indigo 运行时内置的标准绑定;C 是 contractType="ITransactionalWebService"
。请注意,我们修改了标准的 wsProfileBinding
以设置 flowTransactions=ContextFlowOption.Required
,因为 WS-AT 需要通过 Indigo 将事务上下文流动到另一台物理机。
为了处理物理机 2 上的 Indigo ABC,我使用 VS.NET 2005 Feb CTP Indigo 服务项目模板生成了一个特殊的 Web 虚拟目录“TransactionalWS”,其中包含一个子目录“App_Code”和三个文件。
\TransactionalWS
\App_Code
Service.cs
Service.svc
Web.Config
Service.svc 的内容与 Service.asmx 类似。
<%@ Service Language="C#"
CodeBehind="~/App_Code/Service.cs" Class="MyService" %>
Service.cs 是代码隐藏文件。
[ServiceContract()]
public interface ITransactionalWebService
{
[OperationContract]
[OperationBehavior(AutoCompleteTransaction =true,AutoEnlistTransaction =true)]
void DoDBTransaction(int OrderID);
[OperationContract]
[OperationBehavior(AutoCompleteTransaction = true, AutoEnlistTransaction = true)]
void DoNonDBTransaction(int CustomerID);
}
public class MyService : ITransactionalWebService
{
public void DoDBTransaction(int OrderID) {...}
public void DoNonDBTransaction(int CustomerID) { ….}
}
Web.Config 包含以下内容。
<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
<system.serviceModel>
<services>
<service serviceType="MyService">
<endpoint contractType="ITransactionalWebService"
bindingSectionName="wsProfileBinding"
bindingConfiguration="wsProfileConfig"/>
</service>
</services>
<behaviors>
<behavior configurationName="" returnUnknownExceptionsAsFaults="true" />
</behaviors>
<bindings>
<wsProfileBinding>
<binding flowTransactions="Required" configurationName="wsProfileConfig"/>
</wsProfileBinding>
</bindings>
</system.serviceModel>
</configuration>
这里的 A 是虚拟目录地址 https:///TransactionWS/Service.svc;B 是 wsProfileBinding
,并进行了 flowTransactions="Required"
修改;C 是 ITransactionalWebService
。
理想情况下,我也希望在示例应用程序的 Indigo 事务 Web 服务端使用代码而不是配置。但是,我无法使用其他主机(如控制台、Windows 服务)稳定地使其工作。因此,我决定继续将 Indigo 服务托管在这种 Web 目录结构中。Indigo 的未来版本无疑将允许在任何其他类型的宿主中使用 WS-AT。
您可能已经注意到 Indigo Web 服务的两个“行为”修改。
[OperationBehavior(AutoCompleteTransaction = true, AutoEnlistTransaction = true)]
<behavior configurationName="" returnUnknownExceptionsAsFaults="true" />
下面我们简要讨论这些行为更改。
AutoCompleteTransaction= true 或 false
参与的事务 Web 服务可以通过四种方式之一影响协调器:
- 它可以明确投票完成;
- 它可以明确投票回滚;
- 它可以抛出异常;
- 它可以决定不投票(弃权)。
以下是表示上述四种场景的 C# 代码。
public void DoNonDBTransaction(int CustomerID)
{
switch (CustomerID)
{
case 1:
OperationContext.Current.SetTransactionComplete();
break;
case 2:
System.Transactions.Transaction.Current.Rollback(new
Exception("Customer has bad credit"));
break;
case 3:
throw new Exception("Unknown exception" +
" for CustomerID=3 and converted into fault");
break;
default:
// refrain from vote, customer has unknown credit.
// In this case, transaction complete or abort depend on
// AutoCompleteTransaction= true or false.
break;
}
显然,协调器在做出最终决定之前必须知道参与者投票完成还是回滚。这就是 AutoCompleteTransaction
发挥作用的地方。
- 如果
AutoCompleteTransaction =true
,则参与者的弃权意味着它投票完成。 - 如果
AutoCompleteTransaction =false
,则参与者的弃权意味着它投票回滚。
例如,如果我们要在贷款事务上下文中调用信用报告 Web 服务,一个没有信用的客户(因为信用报告 Web 服务无法确定)可能是获得贷款的好客户,也可能是未获得贷款的坏客户,这取决于我们对风险与收入的判断。总之,在现实世界中,设置 AutoCompleteTransaction= true
或 false
都有意义。
值得注意的是,Rollback 实际上会抛出一个已知的异常,Indigo 实际上允许 scope.Complete()
在协调器中执行,然后再抛出异常。对于未知异常,协调器代码将冻结,除非我们设置 returnUnknownExceptionAsFaulst="true"
。尚不清楚为什么此 WinFx CTP 会将此属性默认为“false
”而不是 true
,因为让协调器了解所有异常(即使与事务无关)似乎更合适。
如何设置和运行示例代码
- 在干净的物理 PC 或虚拟机上安装 Windows XP SP2。由于我们处理的是预发布软件,将其与应用程序和数据混合使用不是个好主意。
- 安装 SQL Server 2000 和 Northwind 数据库。请注意,我在示例中使用了 sa 密码=xxxxxx,您可能需要通过更改所有三个配置文件(App.Config、Web.Config 和 TransactionOriginator.exe.config)中的连接字符串来调整它。
- 安装 WinFx March 2005 CTP。请按照 CTP 的安装说明进行操作。请注意,WinFx CTP 可在此处免费下载,而 Visual Studio .NET 2005 Beta 仅面向 MSDN 订阅者。但要运行示例,您不需要 VS.NET 2005。但如果您确实想使用 VS.NET,则该示例仅适用于 VC.NET 2005 Feb 2005 CTP。
- 将示例代码文件解压到 c:\TransactionalWebService 目录,并允许 Web 共享此目录作为 TransactionalWS。
- 使用 IIS MMC 将 TransactionWS 虚拟目录配置为 Web 应用程序,并设置安全以允许匿名和 Windows 集成身份验证。
- 使用 IIS MMC 编辑 .svc 应用程序扩展以允许所有动词:(我在示例代码因“请求错误”、“无效内容类型”等错误而无法运行时执行了此操作。所以这只是我针对预发布软件进行的一次调整,我并不完全理解原因。)
- 重新启动 IIS 并启动 MSDTC 服务(例如:在命令提示符下输入“iisreset”、“net start msdtc”)。
- 打开 DOS 提示符,并切换到 C:\TransactionalWebService\ TransactionalWebService\TransactionOriginator\bin\Debug。
然后运行 TransactionOriginator.exe,您应该会看到类似如下的结果。
- 您可以使用 SQL 查询分析器在 Northwind 数据库上执行“
Select * from Orders
”,您应该会看到前两个OrderDate
已更新。- 对于客户 ID = 1,两个日期已更新,表示完成。
- 对于客户 ID = 2,没有更新,因为 Web 服务中存在显式回滚。
- 对于客户 ID = 3,没有更新,因为存在未知异常。
- 对于客户 ID = 4,两个已更新,表示完成(因为我们将
AutoCompleteTransaction =true
)。
警告:由于这是预发布软件,示例代码可能存在异常行为。例如,我曾遇到示例突然崩溃的情况,不得不重新构建 Web 目录、重新启动 IIS/DTC 等。此外,CTP 或 Beta 的未来版本可能会破坏代码。
结论
Indigo 事务编程相对简单明了,只要我们花足够的时间来理解 WS-AT 模型的基础知识。希望本文及附带的示例代码能证明这一点。