閱讀984 返回首頁    go 技術社區[雲棲]


WCF技術剖析之三十二:一步步創建一個完整的分布式事務應用

在完成了對於WCF事務編程(《上篇》、《中篇》、《下篇》)的介紹後,本篇文章將提供一個完整的分布式事務的WCF服務應用,通過本例,讀者不僅僅會了解到如何編程實現事務型服務,還會獲得其他相關的知識,比如DTC和AS-AT的配置等。本例還是沿用貫通本章的應用場景:銀行轉帳。我們將會創建一個BankingService服務,並將其中的轉帳操作定義成事務型操作。我們先從物理部署的角度來了解一下BankingService服務,以及需要實現怎樣的分布式事務。

一、從部署的角度看分布式事務

既然是實現分布式事務,那麼事務會跨越多台機器。簡單起見,我使用兩台機器來模擬。有條件的讀者可以在自己的局域網中進行練習,如果你沒有局域網可用,你可以使用虛擬機來模擬局域網。假設兩台機器名分別是Foo和Bar,整個應用的物理拓撲結構如圖1所示。

image

圖1 BankingService物理部署拓撲

BankingService和客戶端部署與主機Foo,定義在BankingService的轉賬的兩個子操作“提取(Withdraw)”和“存儲(Deposit)”通過調用部署於主機Bar的同名服務(WithdrawService和DepositService)實現。而WithdrawService和DepositService最終實現對存儲於數據庫(這裏是SQL Server)的數據進行修改,MSSQL部署與主機Bar中。實際上,整個應用主要涉及到對三個服務(BankingService、WithdrawService和DepositService)的實現,我們先來看看服務契約和服務的實現。

步驟1:服務契約和服務的實現

我們仍然采用契約共享的方式將服務契約定義在單獨的項目之中,共服務端和客戶端共享。涉及到的三個服務對應的服務契約定義如下,事務型操作的TransactionFlow選項被設置為Allwed(默認值)。

IBankingService:

   1: using System.ServiceModel;
   2: namespace Artech.TransactionalService.Service.Interface
   3: {
   4:     [ServiceContract(Namespace = "https://www.artech.com/banking/")]
   5:     public interface IBankingService
   6:     {       
   7:         [OperationContract]
   8:         [TransactionFlow(TransactionFlowOption.Allowed)]
   9:         void Transfer(string fromAccountId, string toAccountId, double amount);
  10:     }
  11: }

IWithdrawService:

   1: using System.ServiceModel;
   2: namespace Artech.TransactionalService.Service.Interface
   3: {
   4:     [ServiceContract(Namespace = "https://www.artech.com/banking/")]
   5:     public interface IWithdrawService
   6:     {
   7:         [OperationContract]
   8:         [TransactionFlow(TransactionFlowOption.Allowed)]
   9:         void Withdraw(string accountId, double amount);
  10:     }
  11: }

IDepositService:

   1: using System.ServiceModel;
   2: namespace Artech.TransactionalService.Service.Interface
   3: {
   4:     [ServiceContract(Namespace="https://www.artech.com/banking/")]
   5:     public interface IDepositService
   6:     {
   7:         [OperationContract]
   8:         [TransactionFlow(TransactionFlowOption.Allowed)]
   9:         void Deposit(string accountId, double amount);
  10:     }
  11: }

實現了服務操作的IWithdrawService和IDepositService的WithdrawService和DepositService分別實現基於給定銀行賬戶的提取和存儲操作。限於篇幅的問題,具體對數據庫相應數據的更新操作就不再這裏一一介紹了。下麵是WithdrawService和DepositService的定義,由於不管是單獨被調用,還是作為轉帳的一個子操作,Withdraw和Deposit操作均需要在一個事務中執行,所以我們需要通過應用OperationBehaviorAttribute將TransactionScopeRequired屬性設為True。

WithdrawService:

   1: using System.ServiceModel;
   2: using Artech.TransactionalService.Service.Interface;
   3: namespace Artech.TransactionalService.Service
   4: {
   5:     public class WithdrawService : IWithdrawService
   6:     {
   7:         [OperationBehavior(TransactionScopeRequired = true)]
   8:         public void Withdraw(string accountId, double amount)
   9:         {
  10:             //省略實現
  11:         }
  12:     }
  13: }

DepositService:

   1: using System.ServiceModel;
   2: using Artech.TransactionalService.Service.Interface;
   3: namespace Artech.TransactionalService.Service
   4: {
   5:       public class DepositService : IDepositService
   6:     {       
   7:         [OperationBehavior(TransactionScopeRequired = true)]
   8:         public void Deposit(string accountId, double amount)
   9:         {
  10:              //省略實現
  11:         }
  12:     }
  13: }

定義在BankingService的Transfer操作就是調用上述的兩個服務,由於服務調用設置到對服務代理的關閉以及異常的處理(相關的內容在《WCF技術剖析(卷1)》的第8章有詳細的介紹),為了實現代碼的複用,我定義了一個靜態的ServiceInvoker類。ServiceInvoker定義如下,泛型方法Invoke<TChannel>用於進行服務的調用,並實現了服務代理的關閉(Close),以及異常拋出是對服務代理的中止(Abort)。Invoke<TChannel>的泛型參數類型為服務契約類型,方法接受兩個操作,委托action代表服務調用操作,endpointConfigurationName表示配置的終結點名稱。

   1: using System;
   2: using System.ServiceModel;
   3: namespace Artech.TransactionalService.Service.Interface
   4: {
   5:     public static class ServiceInvoker
   6:     {
   7:         public static void Invoke<TChannel>(Action<TChannel> action, string endpointConfigurationName)
   8:         {
   9:             Guard.ArgumentNotNull(action, "action");
  10:             Guard.ArgumentNotNullOrEmpty(endpointConfigurationName, "endpointConfigurationName");
  11:  
  12:             using (ChannelFactory<TChannel> channelFactory = new ChannelFactory<TChannel>(endpointConfigurationName))
  13:             {
  14:                 TChannel channel = channelFactory.CreateChannel();
  15:                 using (channel as IDisposable)
  16:                 {
  17:                     try
  18:                     {
  19:                         action(channel);
  20:                     }
  21:                     catch (TimeoutException)
  22:                     {
  23:                         (channel as ICommunicationObject).Abort();
  24:                         throw;
  25:                     }
  26:                     catch (CommunicationException)
  27:                     {
  28:                         (channel as ICommunicationObject).Abort();
  29:                         throw;
  30:                     }
  31:                 }
  32:             }
  33:         }
  34:     }
  35: }

那麼,借助於ServiceInvoker,BankingService的定義就很簡單了。對於Transfer操作,我們依然通過OperationBehaviorAttribute特性將TransactionScopeRequired設置成True。

   1: using System;
   2: using System.Collections.Generic;
   3: using System.ServiceModel;
   4: using Artech.TransactionalService.Service.Interface;
   5: namespace Artech.TransactionalService.Service
   6: {
   7:     public class BankingService : IBankingService
   8:     {        
   9:         [OperationBehavior(TransactionScopeRequired = true)]
  10:         public void Transfer(string fromAccountId, string toAccountId, double amount)
  11:         {
  12:             ServiceInvoker.Invoke<IWithdrawService>(proxy => proxy.Withdraw(fromAccountId, amount), "withdrawservice");
  13:             ServiceInvoker.Invoke<IDepositService>(proxy => proxy.Deposit(toAccountId, amount), "depositservice");
  14:         }
  15:     }
  16: }

步驟2:部署服務

BankingService和依賴的WithdrawService與DepositService已經定義好了,現在我們需要對它們進行部署。本實例采用基於IIS的服務寄宿方式,在進行部署之前需要為三個服務創建.svc文件。在這裏,我.svc文件命名為與服務類型相同的名稱(BankingService.svc、WithdrawService.svc和DepositService.svc)。關於.svc文件的具體定義,在這裏就不再重複介紹了,對此不了解的讀者,可以參閱《WCF技術剖析(卷1)》第7章關於IIS服務寄宿部分。

我們需要分別在主機Foo和Bar上創建兩個IIS虛擬目錄(假設名稱為Banking),並將定義服務契約和服務類型的兩個程序集拷貝到Foo\Banking\Bin和Bar\Banking\Bin。然後再將BankingService.svc拷貝到Foo\ Banking下,將WithdrawService.svc和DepositService.svc拷貝到Bar\Banking下。

最後,我們需要創建兩個Web.config,分別拷貝到Foo\Banking\Bin和Bar\Banking下麵。下麵兩段XML代表兩個Web.config的配置。

Foo\Banking\Web.config:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>  
   3:     <system.serviceModel>
   4:         <bindings>
   5:           <customBinding>
   6:             <binding name="transactionalBinding">
   7:               <textMessageEncoding />
   8:               <transactionFlow/>
   9:               <httpTransport/>
  10:             </binding>
  11:           </customBinding>
  12:         </bindings>
  13:         <services>
  14:             <service name="Artech.TransactionalService.Service.WithdrawService">
  15:                 <endpoint  binding="customBinding" bindingConfiguration="transactionalBinding" contract="Artech.TransactionalService.Service.Interface.IWithdrawService" />
  16:             </service>
  17:           <service name="Artech.TransactionalService.Service.DepositService">
  18:             <endpoint binding="customBinding" bindingConfiguration="transactionalBinding" contract="Artech.TransactionalService.Service.Interface.IDepositService" />
  19:           </service>
  20:         </services>     
  21:     </system.serviceModel>
  22: </configuration>

Bar\Banking\Web.config:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>  
   3:   <system.serviceModel>
   4:     <bindings>
   5:       <customBinding>
   6:         <binding name="transactionalBinding">
   7:           <textMessageEncoding />
   8:           <transactionFlow/>
   9:           <httpTransport/>
  10:         </binding>
  11:       </customBinding>
  12:     </bindings>
  13:     <services>
  14:       <service name="Artech.TransactionalService.Service.BankingService">
  15:         <endpoint  binding="customBinding" bindingConfiguration="transactionalBinding" contract="Artech.TransactionalService.Service.Interface.IBankingService" />
  16:       </service>
  17:     </services>
  18:     <client>
  19:       <endpoint name="withdrawservice" address="https://Bar/banking/withdrawservice.svc"  binding="customBinding" bindingConfiguration="transactionalBinding"
  20:             contract="Artech.TransactionalService.Service.Interface.IWithdrawService" />
  21:       <endpoint name="depositservice" address="https://Bar/banking/depositservice.svc"  binding="customBinding" bindingConfiguration="transactionalBinding"
  22:           contract="Artech.TransactionalService.Service.Interface.IDepositService" />
  23:     </client>
  24:   </system.serviceModel>
  25: </configuration>

步驟3:調用BankingService

現在我們已經部署好了定義的三個服務,現在我們可以調用它們實施轉帳處理了。我們可以像調用普通服務一樣調用BankingService,無須考慮事務的問題。因為我們通過OperationBehaviorAttribute特性將BankingService的Transfer操作的TransactionScopeRequired設置成True,這會確保整個操作的執行是在一個事務中進行(可能是流入的事務,也可能是重新創建的事務)。下麵進行轉帳處理的客戶端代碼和配置。

   1: string fromAccountId = "123456789";
   2: string tooAccountId = "987654321";
   3: double amount = 1000;
   4: ServiceInvoker.Invoke<IBankingService>(proxy => proxy.Transfer(fromAccountId, tooAccountId, amount), "bankingservice");

配置:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:   <system.serviceModel>
   4:     <bindings>
   5:       <customBinding>
   6:         <binding name="transactionalBinding">
   7:           <textMessageEncoding />
   8:           <transactionFlow />
   9:           <httpTransport/>
  10:         </binding>
  11:       </customBinding>
  12:     </bindings>   
  13:     <client>
  14:       <endpoint address="https://Foo/banking/bankingservice.svc"
  15:        binding="customBinding" bindingConfiguration="transactionalBinding" contract="Artech.TransactionalService.Service.Interface.IBankingService"
  16:        name="bankingservice" />      
  17:     </client>
  18:   </system.serviceModel>
  19: </configuration>

實際上,由於定義在BankingService的Transfer操作完全是通過調用WithdrawService和DepositService實現的,我們也可以繞過BankingService直接調用這兩個服務實現轉賬的處理。為此我們需要在配置中添加調用WithdrawService和DepositService的終結點:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:   <system.serviceModel>
   4:     ......
   5:     <client>
   6:       <endpoint name="withdrawservice" address="https://Bar/banking/withdrawservice.svc"  binding="customBinding" bindingConfiguration="transactionalBinding"            contract="Artech.TransactionalService.Service.Interface.IWithdrawService" />
   7:       <endpoint name="depositservice" address="https://Bar/banking/depositservice.svc"  binding="customBinding" bindingConfiguration="transactionalBinding"          contract="Artech.TransactionalService.Service.Interface.IDepositService" />
   8:     </client>
   9:   </system.serviceModel>
  10: </configuration>

由於整個轉帳的操作必須納入到一個事務中進行,並且客戶端主動發起對WithdrawService和DepositService兩個服務的調用,所以客戶端是事物的初始化者。為此,我們需要將對這兩個服務的調用放到一個TransactionScope中進行,相應的代碼如下所示:

   1: string fromAccountId = "123456789";
   2: string tooAccountId = "987654321";
   3: double amount = 1000;
   4: using (TransactionScope transactionScope = new TransactionScope())
   5: {
   6:     ServiceInvoker.Invoke<IWithdrawService>(proxy => proxy.Withdraw(fromAccountId, 100), "withdrawservice");
   7:     ServiceInvoker.Invoke<IDepositService>(proxy => proxy.Deposit("B001", 100), "depositservice");
   8:     transactionScope.Complete();
   9: }

對於上麵兩種不同的實現方式,實際上都已經涉及到了分布式事務的應用,所以需要借助於DTC。如果讀者在運行該實例的時候,兩個主機的DTC沒有進行合理的設置,將不會成功運行,現在我們簡單介紹一下如何進行DTC的設置。

步驟4:設置DTC

通過“控製麵板”|“管理工具”|“組件服務”打開組件服務的對話框。然後右擊“組件服務”\“計算機”\“我的電腦”結點,並在的上下文菜單中選擇“屬性”,會彈出“我的電腦 屬性”對話框。在該對話框的“MSDTC”Tab頁,選擇默認的協調器,一般地我們選擇“使用本地協調器”選項。

當我們選擇使用本地協調器作為默認的DTC之後,在組件服務對話框的“組件服務”\“計算機”\“我的電腦”\“Distributed Transaction Coordinator”結點下麵會出現“本地DTC”結點。右擊該節點選擇“屬性”選項,會彈出如圖2所示的“本地DTC屬性”。你可以對DTC的跟蹤(Trace)方式、日誌記錄、安全和WS-AT進行相應的設置。在這裏要是DTC在本實例中可用,重點是對“安全”進行正確的設置。圖2是我機器上的設置,限於篇幅問題,我不能對每一個選項進行詳細說明,有興趣的讀者相信很容易從網上找到相關的參考資料。此外,如果在已經對DTC作了相應設置之後還出現DTC的問題,你可以看看DTC通信是否被防火牆屏蔽。

image

圖2 本地DTC設置對話框

步驟5:采用WS-AT協議

在本例中,所有終結點采用的綁定類型均是包含有TransactionFlowBindingElement的CustomBinding。通過前麵的介紹我們知道,默認采用的事務處理協議是OleTx,如果我們希望采用WS-AT協議,我們需要通過配置將協議類型改成WSAtomicTransactionOctober2004或者WSAtomicTransaction11。下麵的配置中,我們將實例中使用的綁定支持的事務處理協議設置成WSAtomicTransaction11,使之采用WS-AT協議進行事務處理。

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:   <system.serviceModel>
   4:     <bindings>
   5:       <customBinding>
   6:         <binding name="TransactionalBinding">
   7:           <textMessageEncoding />
   8:           <transactionFlow transactionProtocol="WSAtomicTransaction11" />
   9:           <httpTransport/>
  10:         </binding>
  11:       </customBinding>
  12:     </bindings>
  13:   </system.serviceModel>
  14: </configuration>

DTC通過基於HTTPS的通信方式為WS-AT(1.0和1.1)提供實現,我們隻知道HTTPS通過SSL實現傳輸安全,而SSL需要將相應的證書(Certificate)綁定在HTTPS站點上麵。為了讓DTC支持WS-AT,我們需要對DTC進行相關的配置。

首先我們需要為參與到事務的兩台主機創建相對應的證書,在這裏我們直接采用Makecert.exe這個X.509證書生成工具。我們可以通過下麵兩個命令行創建兩張X.509證書,它們分別代表Foo和Bar兩台主機,讀者在做練習這個例子時,需要換成自己相應的機器名稱。關於Makecert.exe的命令行選項的含義,可以參考MSDN(https://msdn.microsoft.com/zh-cn/library/bfsktky3%28VS.80%29.aspx)

MakeCert –n CN=Foo –pe –sky exchange –sr LocalMachine –ss MY
MakeCert –n CN=Bar –pe –sky exchange –sr LocalMachine –ss MY

在主機Foo運行上麵的命令行後,會創建兩張個X.509證書並將其存入“本地計算機\個人(LocalMachine\MY)”存儲中。你需要通過MMC將其導出成證書文件,並將其導入到主機Foo的“本地計算機\受信任的根證書頒發機構(LocalMachine\Root)”存儲中,以及主機Bar的“本地計算機\個人(LocalMachine\MY)”和“本地計算機\受信任的根證書頒發機構(LocalMachine\Root)”存儲中。需要注意的是,在到處的時候務必選擇“導出私鑰”選項,因為不包含私鑰的正式是不能和SSL站點綁定的。

DTC的WS-AT可以借助WS-AT配置工具wsatConfig .exe以命令行的方式進行設置,該工具位於"%WINDIR%\Microsoft.NET\Framework\v3.0\Windows Communication Foundation"目錄下麵。關於wsatConfig.exe配置工具的用法,可以參考MSDN相關文檔:https://msdn.microsoft.com/zh-cn/library/ms732007.aspx。在這裏,我需要介紹的是一種可視化的WS-AT配置方式。

圖2所示的DTC設置對話框中,有一個WS-AT Tab頁,通過它我們可以很容易地進行WS-AT的相關配置。不過,在默認的情況下,這個Tab頁是不存在的。我們需要借助Regasm.exe這個程序集注冊工具以命令行的形式對包含有WS-AT配置界麵的程序集進行注冊,該程序集位於Windows SDK目錄下:%PROGRAMFILES%\Microsoft SDKs\Windows\v6.0\Bin或者%PROGRAMFILES%\Microsoft SDKs\Windows\v6.0A\Bin。當運行下麵的命令行後,DTC設置對話框中將會出現WS-AT Tab頁了。

regasm.exe /codebase WsatUI.dll 

選擇WS-AT Tab,你將會看到如圖3所示的界麵。然後,我們針對主機Foo和Bar分別進行如下的設置,使之建立相互信任關係:

  • Foo選擇證書Foo(CN=Foo)和Bar(CN=Bar)分別作為終結點證書(Endpoint certificate)和授權證書(Authorized certificates);
  • Bar選擇證書Bar(CN=Bar)和Foo(CN=Foo)分別作為終結點證書(Endpoint certificate)和授權證書(Authorized certificates)。

image

圖3 WS-AT設置界麵

對於本實例來說,既是你將綁定的事務處理協議設置成WSAtomicTransactionOctober2004或者WSAtomicTransaction11,並對WS-AT進行了正確的設置,但是依然采用的是OleTx協議。這是由於DTC的OleTx提升(OleTx Upgrade)機製導致。關於OleTx提升機製,會在本章後續部分介紹。如果希望本實例真正采用WS-AT進行事務事務,你需要顯示關閉OleTx的自動提升。我們隻需要通過注冊表編輯器在HKLM\SOFTWARE\Microsoft\WSAT\3.0結點下添加一個名稱為OleTxUpgradeEnabled的雙字節(DWORD)注冊表項,比較值設為0。


作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-27 16:04:28

  上一篇:go  WCF技術剖析之三十一: WCF事務編程[下篇]
  下一篇:go  WCF技術剖析之三十三:你是否了解WCF事務框架體係內部的工作機製?[上篇]