1、WCF分布式事务例子

这里也用转账的例子说事。

用户在系统A和系统B都有账户,账户间的资金可以互转,系统A的资金减少多少,系统B的相应账户的资金就增加多少。

系统A机器上有数据库AccountA,系统B机器上有数据库AccountB,数据库的结构一样,都有一个数据表Account,结构如下:

字段

数据类型

含义

depositorID

int

账户id

amount

decimal(18, 2)

金额


为了演示TxF事务性文件,在系统B中增加了一个写文件的操作,记录本次转账操作的信息。转账的所有操作步骤:系统A上账户上减少金额,系统B上记录转账信息文件,系统B上相应账户资金增加这三个操作都在一个事务流中,要么全部完成,要么全部回滚。

系统A和系统B分别在服务器A和服务器B上。系统A在账户上减少金额后调用系统BWCF服务,在系统B中继续增加账户资金,生成转账信息文件。

下面开始这个例子的完整过程。

1.1.建立系统B转账WCF服务

建立服务契约:

[ServiceContract]

public interface IAccountB

{

[OperationContract]

[TransactionFlow(TransactionFlowOption.Allowed)]

void deposit(int depositorid, double amount);

}

服务契约就一个方法deposit,其中depositorid表示账户idamount表示要从系统A转账到系统B的金额。

TransactionFlow这个属性指示operation是否跟随调用端的事务流,参数含义:

TransactionFlowOption.NotAllowed 表示此operation不跟随传入的事务流,不参与分布式事务。

TransactionFlowOption.Allowed 表示此opreation可以跟随传入的事务流,如果有传入的事务流则参与,如果没有传入的事务流则不参与,但是可以启动本地的事务。

TransactionFlowOption.Mandatory 表示此operation必须跟随传入的事务,参与分布式事务,如果调用此operation的客户端没有事务流则抛出异常。


下面是服务实现:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]

public class AccountBService : IAccountB

{

[OperationBehavior(TransactionScopeRequired = true)]

public void deposit(int depositorid, double amount)

{

#region新建事务性文件

string path = @"c:\test.txt";

FileStream fs = TransactedFile.Open(path, System.IO.FileMode.Create,

System.IO.FileAccess.ReadWrite, System.IO.FileShare.ReadWrite);

string fileContent = string.Format("从系统A转账到系统B\r\n用户ID:{0}\r\n转账金额为:{1}",depositorid.ToString(), amount.ToString());

byte[] byteArrar = Encoding.UTF8.GetBytes(fileContent);

fs.Write(byteArrar, 0, byteArrar.Count());

fs.Flush();

fs.Close();

#endregion


#region数据访问,在指定账户上增加存款

string connstr = ConfigurationManager.ConnectionStrings["ConnStr"].ToString();

SqlCommand mySqlCommand = new SqlCommand("update account set amount = amount +@amount where depositorid = @depositorid ");

mySqlCommand.Connection = new SqlConnection(connstr);

SqlParameter par1 = new SqlParameter("@amount", SqlDbType.Decimal);

par1.Value = amount;

mySqlCommand.Parameters.Add(par1);

par1 = new SqlParameter("@depositorid", SqlDbType.Int);

par1.Value = depositorid;

mySqlCommand.Parameters.Add(par1);

mySqlCommand.Connection.Open();

mySqlCommand.ExecuteNonQuery();

mySqlCommand.Connection.Close();

#endregion

}

}

服务实现了deposit操作。

[OperationBehavior(TransactionScopeRequired = true)],这里的TransactionScopeRequired = true表示这个操作在TransactionScope内执行,加上前面OperationContract上的TransactionFlowOption.Allowed 允许跟随事务的设置,这个deposit的操作将会参与客户端发起的分布式事务。

实现的deposit操作中完成两个任务,先转账信息写入c:\test.txt文件,这里写文件操作使用了TxF事务性文件操作TransactedFile.Open关于TxF的操作部分的代码微软有提供,在本文中提供的代码中包含了这部分源码。使用事务性文件操作,在事务中的其他事务资源操作失败后,文件操作也会回滚。


1.2.建立系统A转账客户端

系统A是个Console应用:

static void Main(string[] args)

{

ChannelFactory<IAccountB> myFactory = new ChannelFactory<IAccountB>("endpointConfig");

IAccountB myClient = myFactory.CreateChannel();

Double amount = 500;

int depositorid = 1;

using (TransactionScope scop = new TransactionScope())

{

#region数据访问,在指定账户上减少存款

string connstr = ConfigurationManager.ConnectionStrings["ConnStr"].ToString();

SqlCommand mySqlCommand = new SqlCommand("update account set amount = amount - @amount where depositorid = @depositorid ");

mySqlCommand.Connection = new SqlConnection(connstr);

SqlParameter par1 = new SqlParameter("@amount", SqlDbType.Decimal);

par1.Value = amount;

mySqlCommand.Parameters.Add(par1);

par1 = new SqlParameter("@depositorid", SqlDbType.Int);

par1.Value = depositorid;

mySqlCommand.Parameters.Add(par1);

mySqlCommand.Connection.Open();

mySqlCommand.ExecuteNonQuery();

mySqlCommand.Connection.Close();

#endregion

try

{

myClient.deposit(depositorid, amount);

scop.Complete();

}

catch (Exception e)

{

Transaction.Current.Rollback();

}

}

}


1.3.配置使用OleTransactions协议

先测试使用OleTransactions分布式事务协议,下面是使用OleTransactions时在客户端和服务端需要的服务和配置。

1.3.1.配置文件

系统BWCF服务的配置文件:

<?xmlversion="1.0"encoding="utf-8" ?>

<configuration>

<system.web>

<compilationdebug="true" />

</system.web>

<connectionStrings>

<addname="ConnStr"connectionString="Server=.;Integrated security=true;initial catalog=AccountB" />

</connectionStrings>

<system.serviceModel>

<bindings>

<customBinding>

<bindingname="customBindingConfig">

<transactionFlowtransactionProtocol="OleTransactions" />

<httpTransport />

</binding>

</customBinding>

</bindings>

<services>

<servicebehaviorConfiguration="WCF_ATTransTest.Service.Service1Behavior"

name="WCF_ATTransTest.Service.AccountBService">

<endpointaddress=""binding="customBinding"bindingConfiguration="customBindingConfig"

name="serviesEndpoint"contract="WCF_ATTransTest.Service.IAccountB" />

<host>

<baseAddresses>

<addbaseAddress="http://localhost/WCF_ATTransTest.Service/AccountBService/" />

</baseAddresses>

</host>

</service>

</services>

<behaviors>

<serviceBehaviors>

<behaviorname="WCF_ATTransTest.Service.Service1Behavior">

<serviceMetadatahttpGetEnabled="true"/>

<serviceDebugincludeExceptionDetailInFaults="true" />

</behavior>

</serviceBehaviors>

</behaviors>

</system.serviceModel>

</configuration>

系统A WCF客户端的配置文件

<?xmlversion="1.0"encoding="utf-8" ?>

<configuration>

<connectionStrings>

<addname="ConnStr"connectionString="Server=.;Integrated security=true;initial catalog=AccountA" />

</connectionStrings>

<system.serviceModel>

<client>

<endpointaddress="http://chnking-pc/WCF-ATTransTest/WCF_ATTransTest.Service.AccountBService.svc"

binding="customBinding"bindingConfiguration="customBindingConfig"

contract="WCF_ATTransTest.Service.IAccountB"name ="endpointConfig"/>

</client>

<bindings>

<customBinding>

<bindingname="customBindingConfig">

<transactionFlowtransactionProtocol="OleTransactions" />

<httpTransport />

</binding>

</customBinding>

</bindings>

</system.serviceModel>

</configuration>


1.3.2.配置RPC

使用OleTx协议时,要通过RPC协议在DTC事务管理器之间通讯,RPC本身使用135端口,还要使用数量不定的1024端口以上的动态端口。所以防火墙必须要开放135端口和1024端口以后的所有端口。

但是要开放所有1024以上的端口又是很危险的事情,最好能把RPC使用的端口限制在一定的范围,然后防火墙只开放这个范围的端口。

可以通过修改注册表来达到这个目的:

运行Regedt32.exe打开注册表,在以***册表项下添加 Internet 项:HKEY_LOCAL_MACHINE\Software\Microsoft\Rpc,在 Internet 项下,添加值“Ports(MULTI_SZ)、“PortsInternetAvailable(REG_SZ) 和“UseInternetPorts(REG_SZ)

在本示例中,使用了端口 5000 5100(含 5000 5100),因此该新注册表项将显示为以下形式:

Ports:REG_MULTI_SZ: 5000-5100

PortsInternetAvailable:REG_SZ:Y

UseInternetPorts:REG_SZ:Y

      clip_image002

在所有参与事务的机器上都必须启动RPC服务,并配置RPC使用的动态端口,防火墙开放这些动态端口。

1.3.3.DTC服务

发起事务的服务器的DTC服务必须启动,参与事务的服务器的DTC服务可以不启动。

在事务内要调用跨越进程或机器的服务,事务需要提升级别到DTC管理器,如果发起事务的服务器的DTC服务未启动,则会抛出异常:“试图提升事务时失败。服务器xxx上的MSDTC不可用。”

1.3.4.测试

将系统BWCF服务发布至IIS,系统A调用WCF的地址指向系统B发布的服务地址,测试结果,发送到WCF的消息和返回的消息:

<s:Envelopexmlns:s="http://www.w3.org/2003/05/soap-envelope"xmlns:a="http://www.w3.org/2005/08/addressing">

<s:Header>

<a:Actions:mustUnderstand="1">http://tempuri.org/IAccountB/deposit</a:Action>

<a:MessageID>urn:uuid:6d8931ab-f79e-4de8-a377-4d8acdcf3545</a:MessageID>

<a:ReplyTo>

<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>

</a:ReplyTo>

<OleTxTransactions:mustUnderstand="1"xmlns="http://schemas.microsoft.com/ws/2006/02/tx/oletx">

<PropagationToken>AQAAAAMAAACxK7vUESf1RJ8BoxrUWJUvAAAQAAAAAABsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGU0NDljY2MzLWY3OWQtNDVjMi05YjQzLTI1OTRmY2JhZTQ2NwAALAAHAAAAZM1kzSEAAABXSU43LVBDABAAAABXAEkATgA3AC0AUABDAAAAAQAAAAAAAAAOAAAAdGlwOi8vV2luNy1QQy8AAA==

</PropagationToken>

</OleTxTransaction>

<a:Tos:mustUnderstand="1">http://chnking-pc/WCF-ATTransTest/WCF_ATTransTest.Service.AccountBService.svc</a:To>

</s:Header>

<s:Body>

<depositxmlns="http://tempuri.org/">

<depositorid>1</depositorid>

<amount>500</amount>

</deposit>

</s:Body>

</s:Envelope>

注意在请求消息的header中有<OleTxTransaction>标签,这个标签用来标示采用OleTxTransaction分布式事务协议。

<s:Envelopexmlns:s="http://www.w3.org/2003/05/soap-envelope"xmlns:a="http://www.w3.org/2005/08/addressing">

<s:Header>

<a:Actions:mustUnderstand="1">http://tempuri.org/IAccountB/depositResponse</a:Action>

<a:RelatesTo>urn:uuid:6d8931ab-f79e-4de8-a377-4d8acdcf3545</a:RelatesTo>

</s:Header>

<s:Body>

<depositResponsexmlns="http://tempuri.org/"/>

</s:Body>

</s:Envelope>


1.4.配置使用WS-AtomicTransaction协议

1.4.1.配置文件

将服务端和客户端的配置文件中的:

<transactionFlowtransactionProtocol="OleTransactions" />

改成:

<transactionFlowtransactionProtocol="WSAtomicTransactionOctober2004" />

1.4.2.配置WS-AT

要让DTC支持WS-AT协议需要配置参与分布式事务的所有机器的证书,安装WS-ATDTCUI等等,配置过程还比较复杂,详细步骤参见微软文档:配置 WS-Atomic 事务支持(http://msdn.microsoft.com/zh-cn/library/ms733943.aspx

配置好的界面如下:

clip_image004


WS-AT事务管理器之间的通讯使用SSL安全通道传输,所以每台参与WS-AT事务的机器都必须配置证书和相应的https的端口。

WS-ATUI界面虽然安装在DTC中,但是WS-AT事务并不依赖于DTC服务,所以所有参与WS-AT事务的机器的DTC服务不必启动。

1.4.3.测试

抓通讯数据包,这时的消息如下:

<s:Envelopexmlns:s="http://www.w3.org/2003/05/soap-envelope"xmlns:a="http://www.w3.org/2005/08/addressing">

<s:Header>

<a:Actions:mustUnderstand="1">http://tempuri.org/IAccountB/deposit</a:Action>

<a:MessageID>urn:uuid:1c111341-b7ef-4e21-91b9-a00de9aa8eea</a:MessageID>

<a:ReplyTo>

<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>

</a:ReplyTo>

<CoordinationContexts:mustUnderstand="1"xmlns="http://schemas.xmlsoap.org/ws/2004/10/wscoor"xmlns:mstx="http://schemas.microsoft.com/ws/2006/02/transactions">

<wscoor:Identifierxmlns:wscoor="http://schemas.xmlsoap.org/ws/2004/10/wscoor">urn:uuid:c7932c29-6b96-4961-b6d5-ce3507a54f1e</wscoor:Identifier>

<Expires>3600000</Expires>

<CoordinationType>http://schemas.xmlsoap.org/ws/2004/10/wsat</CoordinationType>

<RegistrationService>

<Addressxmlns="http://schemas.xmlsoap.org/ws/2004/08/addressing">https://win7-pc:1443/WsatService/Registration/Coordinator/</Address>

<ReferenceParametersxmlns="http://schemas.xmlsoap.org/ws/2004/08/addressing">

<mstx:RegisterInfo>

<mstx:LocalTransactionId>c7932c29-6b96-4961-b6d5-ce3507a54f1e</mstx:LocalTransactionId>

</mstx:RegisterInfo>

</ReferenceParameters>

</RegistrationService>

<mstx:IsolationLevel>0</mstx:IsolationLevel>

<mstx:LocalTransactionId>c7932c29-6b96-4961-b6d5-ce3507a54f1e</mstx:LocalTransactionId>

<PropagationTokenxmlns="http://schemas.microsoft.com/ws/2006/02/tx/oletx">AQAAAAMAAAApLJPHlmthSbbVzjUHpU8eAAAQAAAAAABsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGU0NDljY2MzLWY3OWQtNDVjMi05YjQzLTI1OTRmY2JhZTQ2NwAALAAHAAAAZM1kzSEAAABXSU43LVBDABAAAABXAEkATgA3AC0AUABDAAAAAQAAAAAAAAAOAAAAdGlwOi8vV2luNy1QQy8AAA==</PropagationToken>

</CoordinationContext>

<a:Tos:mustUnderstand="1">http://chnking-pc/WCF-ATTransTest/WCF_ATTransTest.Service.AccountBService.svc</a:To>

</s:Header>

<s:Body>

<depositxmlns="http://tempuri.org/">

<depositorid>1</depositorid>

<amount>500</amount>

</deposit>

</s:Body>

</s:Envelope>

可以看出使用WS-AT协议时,消息的header部分比使用OleTransactions协议时复杂的多,注意header部分里的:

<Addressxmlns="http://schemas.xmlsoap.org/ws/2004/08/addressing">https://win7-pc:1443/WsatService/Registration/Coordinator/</Address>

这是发起WS-AT事务机器的WS-AT事务管理器的地址,参与WS-AT事务的别的机器都要向这个地址来注册本地事务和报告本地事务的完成情况,以便WS-AT事务管理器协调整个分布式事务,来决定提交整个事务还是回滚整个事务。

返回消息:

<s:Envelopexmlns:s="http://www.w3.org/2003/05/soap-envelope"xmlns:a="http://www.w3.org/2005/08/addressing">

<s:Header>

<a:Actions:mustUnderstand="1">http://tempuri.org/IAccountB/depositResponse</a:Action>

<a:RelatesTo>urn:uuid:1c111341-b7ef-4e21-91b9-a00de9aa8eea</a:RelatesTo>

</s:Header>

<s:Body>

<depositResponsexmlns="http://tempuri.org/"/>

</s:Body>

</s:Envelope>


但是在抓数据包是发现一个问题,就是从消息的header看是显示使用WS-AT协议,但是从抓的数据包看依然是使用的OleTransactions协议处理的事务,通过RPC协议协调事务,下面是部分截图:

       clip_image006

DCERPC显示是RPC协议,ctx_id: 1 906b0ce0-c70b-1067-b317-00dd010662da显示了事务id

原来由于OleTransactions协议的开销远比WS-AT协议小,所以微软的默认设计是即使指定使用WS-AT协议,仍然义无反顾的使用OleTransactions协议。所以这时你的机器上要是DTCRPC的端口没有配置好,就会报错。

要想真正使用WS-AT协议需要手工在注册表中增加一个key

在注册表HKLM\SOFTWARE\Microsoft\WSAT\3.0下增加一个名为“OleTxUpgradeEnabled”的DWord值,这个key表示在指定WS-AT协议时是否升级使用OleTransactions协议,0表示不升级(即表示使用WS-AT协议),1表示升级(即表示使用OleTransactions协议),默认是1,所以默认时即使你指定使用WS-AT协议,实际上还是被OleTransactions协议替换了。设置为0即可使用WS-AT协议了。

使用WS-AT协议时,事务协调不依赖于DTC,也不走RPC协议,只通过WS-AT配置中的https端口通过SSL进行事务管理器之间的通讯。WS-AT协议是业界标准,只要实现了这个协议的系统都能进行互操作,当然不能依赖于微软的专有RPC协议。


代码下载:

WCF-ATTransTest客户端.rar

WCF-ATTransTest服务端.rar