本文章是由機器翻譯。

基礎內容

輕鬆將事務應用於服務

Juval Lowy

代碼下載位置:MSDN 代碼庫
線上流覽代碼

目錄

狀態管理和事務
單調用事務性服務
實例管理和事務
基於會話的服務和 VRM
事務性持久服務
事務性行為
向 IPC 綁定添加上下文
InProcFactory 和事務

程式設計中的一個根本問題就是錯誤恢復。 發生錯誤後,應用程式必須自行恢復到產生錯誤之前的狀態。 請考慮這樣一個應用程式,它試圖執行一項由若干個更小操作組成的操作,這些小操作可能並行發生,而且每個單獨操作的成功或失敗都與其他操作無關。 任何一個更小操作出現錯誤,都意味著系統處於不一致的狀態。

以銀行業務應用程式為例,該應用程式通過將資金劃入一個帳戶並從另一帳戶轉出資金,來在兩個帳戶之間轉移資金。 從一個帳戶轉出資金成功但將其劃入另一帳戶失敗就屬於不一致的狀態,因為資金無法同時存在於兩個位置中;同樣,轉出資金失敗但劃入資金成功也會導致不一致狀態,此時資金失蹤。 通常總是由應用程式通過將系統恢復為原始狀態來從錯誤中恢復。

由於某些原因,做比說要難得多。 首先,對於一項大的操作,如果大量排列部分成功、部分失敗,則操作很快就會變得無法控制。 這將導致代碼容易毀壞,使開發和維護費用變得非常昂貴,且代碼經常在實際中不起作用,其原因是開發人員通常只處理易於恢復的情形,在此類情況下,他們能夠意識到問題所在且知道如何處理。 其次,您的複合操作可能是更大操作的一部分,即使您的代碼執行完美無缺,如果不屬您控制範圍的事物出錯,您也必須撤銷該操作。 這意味著操作的管理和協作參與方之間需要緊密配合。 最後,還需將您的工作與其他需要和系統交互的人員隔離開來,因為如果您以後通過回滾某些操作來從錯誤中恢復,會隱式將其他人員置於錯誤狀態中。

如您所見,手動編寫可靠的錯誤恢復代碼實際上是不可能的,這一點人們早已認識到了。 自二十世紀六十年代在業務環境中使用軟體起,人們就非常清楚必須要有一個更好的方法來管理恢復。 這個方法就是:事務。 事務是一組操作,其任何一個操作的失敗都會導致整組失敗,就象一個原子操作一樣。 使用事務時,無需編寫恢復邏輯,因為沒有要恢復的內容。 當所有操作均成功時,沒有要恢復的內容;當所有操作均失敗時,不會對系統狀態產生任何影響,因此也沒有要恢復的內容。

使用事務時,關鍵是要使用事務性資源管理器。 該資源管理器能夠在事務中止時回滾事務期間所發生的全部更改,並在提交事務時保持更改。 資源管理器還提供隔離功能;也就是說,當事務正在進行中時,資源管理器會阻止所有方(包括事務在內)訪問事務和看到更改(這些更改仍能夠回滾)。 這也意味著,事務永遠不應訪問非資源管理器,因為在事務中止時對此類管理器所做的任何更改都不會回滾,這就需要進行恢復。

傳統意義上,資源管理器為持久資源,如資料庫和訊息佇列。 然而,在 2005 年 5 月刊的《MSDN 雜誌》**中,我在一篇名為“ 無法提交?.NET 中的不穩定資源管理器將事務引入公共類型”的文章仲介紹了實現常用不穩定資源管理器 (VRM)(名為 Transactional<t>)的技術:

public class Transactional<T> : ...
{
   public Transactional(T value);
   public Transactional();
   public T Value
   {get;set;}
   /* Conversion operators to and from T */
}

通過將任何可序列化類型參數(如 int 或 string)指定給 Transactional<t>,將此類型轉換成完備的不穩定資源管理器,該管理器會自動參與到環境事務中、根據事務的結果提交或回滾更改,並將當前更改與所有其他事務隔離開來。

圖 1說明了 Transactional<t> 的用法。 由於範圍不全、事務中止,number 和 city 的值會還原成其事務之前的狀態。

圖 1 使用 Transactional<t>

Transactional<int> number = new Transactional<int>(3);
Transactional<string> city = new Transactional<string>("New York, ");

city.Value += "NY"; //Can use with or without transactions
using(TransactionScope scope = new TransactionScope())
{
   city.Value = "London, ";
   city.Value += "UK";
   number.Value = 4;
   number.Value++;
}
Debug.Assert(number == 3); //Conversion operators at work
Debug.Assert(city == "New York, NY");

除了 Transactional<t> 外,我還為 System.Collections.Generic 中的所有集合提供了一個事務性陣列和事務性版本,如 TransactionalDictionary<K,T>。 這些集合與其非事務性同類是多態關係,它們的用法完全相同。

狀態管理和事務

事務性程式設計的唯一目的是讓系統處於一致狀態。 以 Windows Communication Foundation (WCF) 為例,系統狀態包括資源管理器以及服務實例的記憶體狀態。 雖然資源管理器會在事務結果產生時自動管理其狀態,但對於記憶體中物件或靜態變數而言,情況卻並非如此。

此狀態管理問題的解決方案是開發一項能夠感知狀態的服務,主動管理其狀態。 在事務之間,服務應將其狀態儲存在資源管理器中。 在每個事務開始時,服務應從資源檢索其狀態,並通過此操作使資源參與到事務中。 在事務結束時,服務應將其狀態保存回資源管理器中。 此技術可提供完善的狀態自動恢復功能。 對實例狀態所做的任何更改都會作為事務的一部分提交或回滾。

如果提交事務,則在服務下次獲取其狀態時,它將具有事務後狀態。 如果中止事務,則下次它將具有事務前狀態。 無論採用何種方法,服務都將具有一致的狀態,可供新事務訪問。

編寫事務性服務時,還存在兩個其他問題。 第一個問題是服務如何能夠知道事務開始和結束的時間,以便它可以獲取和保存其狀態。 服務可能屬於一個更大的事務,此類事務跨多個服務和機器。 在兩次調用之間的任一時刻,事務都有可能結束。 誰將調用服務,讓服務知道要保存其狀態? 第二個問題必須利用隔離來解決。 不同的用戶端可能對不同的事務同時調用服務。 服務如何將不同事務對其狀態的更改相互隔離?如果另一個事務要訪問其狀態並根據它的值進行操作,則在原始事務中止且更改回滾時,此事務將處於不一致的狀態。

這兩個問題的解決方案是使方法邊界與事務邊界相等。 在每個方法調用開始時,服務應從資源管理器讀取其狀態;在每個方法調用結束時,服務應將其狀態保存到資源管理器中。 此操作可確保:如事務在兩次方法調用之間結束,可保持服務狀態或將其回滾。 此外,在每個方法調用中,在資源管理器內讀取和存儲狀態可解決隔離難題,因為服務只讓資源管理器在並行事務之間隔離對狀態的訪問。

由於服務使方法邊界與事務邊界相等,因此服務實例還必須在每個方法調用結束時對事務的結果進行表決。 從服務角度而言,方法一旦返回,事務即會完成。 在 WCF 中,這是通過 OperationBehavior 的 TransactionAutoComplete 屬性自動完成的。 當此屬性設置為 true 時,如果操作中沒有未處理的異常,WCF 將自動表決提交事務。 如果有未處理的異常,WCF 將表決中止事務。 由於 TransactionAutoComplete 預設為 true,因此任何事務都將預設使用自動完成方法,如下所示:

//These two definitions are equivalent:
[OperationBehavior(TransactionScopeRequired = true,
                   TransactionAutoComplete = true)]   
public void MyMethod(...)
{...}

[OperationBehavior(TransactionScopeRequired = true)]   
public void MyMethod(...)
{...}

有關 WCF 事務性程式設計的詳細資訊,請參閱 2007 年 5 月刊的“基礎內容”專欄“ WCF 事務傳播”。

單調用事務性服務

使用單調用服務時,一旦調用返回,實例即會銷毀。 因此,用於儲存調用之間狀態的資源管理器必須在實例的範圍之外。 用戶端和服務還必須就哪些操作負責創建實例或從資源管理器中移除實例達成一致。

因為可能有相同服務類型的多個實例訪問同一個資源管理器,所以每個操作均必須包含某個特定參數,以允許服務實例在資源管理器中查找其狀態,並根據它進行綁定。 我將此參數稱為實例 ID。 用戶端還必須調用專用操作,以從存儲中移除實例狀態。 請注意,能夠感知狀態的事務性物件和單調用物件的行為要求相同: 均在方法邊界檢索及保存其狀態。 有了單調用服務,任何資源管理器均可用於儲存服務狀態。 您可以使用資料庫或者使用 VRM,如圖 2 所示。

圖 2 使用 VRM 的單調用服務

[ServiceContract]
interface IMyCounter
{
   [OperationContract]
   [TransactionFlow(TransactionFlowOption.Allowed)]
   void Increment(string instanceId);

   [OperationContract]
   [TransactionFlow(TransactionFlowOption.Allowed)]
   void RemoveCounter(string instanceId);
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyService : IMyCounter
{
   static TransactionalDictionary<string,int> m_StateStore = 
                               new TransactionalDictionary<string,int>();

   [OperationBehavior(TransactionScopeRequired = true)]
   public void Increment(string instanceId)
   {
    if(m_StateStore.ContainsKey(instanceId) == false)
      {
         m_StateStore[instanceId] = 0;
      }
      m_StateStore[instanceId]++;
      Trace.WriteLine(m_StateStore[instanceId]); 
   }
   [OperationBehavior(TransactionScopeRequired = true)]
   public void RemoveCounter(string instanceId)
   {
    if(m_StateStore.ContainsKey(instanceId))
      {
         m_StateStore.Remove(instanceId);
      }
   }
}

//Client side:
MyCounterClient proxy = new MyCounterClient();

using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment("MyInstance");
   scope.Complete();
}    

//This transaction will abort since the scope is not completed 
using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment("MyInstance");
} 

using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment("MyInstance");
   proxy.RemoveCounter("MyInstance");
   scope.Complete();
}

proxy.Close();

//Traces:
1
2
2

實例管理和事務

WCF 強制服務實例使方法邊界與事務邊界相等並且能夠感知狀態,這意味著要在方法邊界清除它的所有實例狀態。 預設情況下,一旦事務完成,WCF 就會銷毀服務實例,確保記憶體中沒有能損害一致性的剩餘內容。

任何事務性服務的生命週期均由 ServiceBehavior 的 ReleaseServiceInstanceOnTransactionComplete 屬性控制。 如果 ReleaseServiceInstanceOnTransactionComplete 設置為 true(預設值),它會在方法完成事務時處理服務實例,這樣一旦涉及到實例程式設計模型,就會將任何 WCF 服務轉換成單調用服務。

這個笨拙的方法並非源自 WCF。 從 MTS 開始,Microsoft 平臺上分佈的所有事務性程式設計模型均通過 COM+ 和 Enterprise Services 使事務性物件等同于單調用物件。 這些技術的架構師只是不相信開發人員能夠在面對事務(既複雜又不直觀的程式設計模型)時正確地管理物件的狀態。 雖然大多數開發人員能夠更方便地使用他們所熟悉的常規 Microsoft .NET Framework 物件的基於會話且有狀態的程式設計模型,但主要缺點是所有想要從事務中受益的開發人員都必須採用有實際意義的單調用程式設計模型(請參見圖 2)。

我個人始終認為使事務等同于單調用產生實體是不得不解決的棘手問題,然而從概念上講,它是被曲解了。 在需要可伸縮性時,人們應該只選擇單調用實例模式;理想情況下,事務應與物件實例管理和應用程式對可伸縮性的考量相隔離。

如果需要擴展您的應用程式,則選擇單調用並使用事務的效果會很好。 然而,如果您不需要可伸縮性(大多數應用程式可能均如此),則不應允許您的服務以會話為基礎、有狀態且是事務性的。 本專欄的其餘部分介紹了我的一項解決方案,它能啟用並保留基於會話的程式設計模型,同時配合使用事務與常用服務。

基於會話的服務和 VRM

WCF 通過將 ReleaseServiceInstanceOnTransactionComplete 設置為 false,允許使用事務性服務維護會話語義。 在此情況下,WCF 將置身事外,讓服務開發人員在面對事務時負責管理服務實例的狀態。 單會話服務仍必須使方法邊界與事務邊界相等,因為每個方法調用可能在不同的事務中,而且事務可能會在同一會話中的兩次方法調用之間結束。 雖然您能夠像使用單調用服務那樣手動管理此狀態(或使用不在本專欄範圍內的某些其他高級 WCF 功能),但您也可以針對服務成員使用 VRM,如圖 3 所示。

圖 3 基於單會話事務性服務使用 VRM

[ServiceBehavior(ReleaseServiceInstanceOnTransactionComplete = false)]
class MyService : IMyContract
{
   Transactional<string> m_Text = new Transactional<string>("Some initial value");
   TransactionalArray<int> m_Numbers = new TransactionalArray<int>(3);

   [OperationBehavior(TransactionScopeRequired = true)]
   public void MyMethod()
   {
      m_Text.Value = "This value will roll back if the transaction aborts";

      //These will roll back if the transaction aborts
      m_Numbers[0] = 11;
      m_Numbers[1] = 22;
      m_Numbers[2] = 33;
   }
}

VRM 的用途是產生有狀態的程式設計模型:服務實例只訪問其狀態,就如同未涉及任何事務一樣。 對狀態所做的任何更改都會利用事務來提交或回滾。 然而,我發現圖 3 是一個專家程式設計模型,因此它有自己的缺陷。 它要求人們熟悉 VRM、成員的細緻化定義以及規定,以將所有操作始終配置為需要事務,並禁止在完成時釋放實例。

事務性持久服務

在 2008 年 10 月刊的這一專欄(“ 使用持久服務管理狀態”)中,我介紹了 WCF 為持久服務所提供的支援。 持久服務從已配置的存儲中檢索其狀態,然後在每次操作時將狀態保存回此存儲中。 狀態存儲不一定是事務性資源管理器。

如果是事務性服務,它當然應該只使用事務性存儲,並且應參與到每個操作的事務中。 採用此方式時,如果事務中止,則狀態存儲會回滾到其事務之前的狀態。 然而,WCF 不知道服務是否設計為將其事務傳播到狀態存儲,而且在預設情況下,它不會讓存儲參與到事務中,即使存儲是事務性資源管理器,如 SQL Server 2005 或 SQL Server 2008。 為指示 WCF 傳播事務並利用基礎存儲,請將 DurableService 的 SaveStateInOperationTransaction 屬性設置為 true:

[Serializable]
[DurableService(SaveStateInOperationTransaction = true)]
class MyService: IMyContract
{...}

SaveStateInOperationTransaction 預設為 false,因而狀態存儲不會參與事務。 由於只有事務性服務能夠從 SaveStateInOperationTransaction 設置為 true 中受益,因此如果它為 true,WCF 便會要求服務上的所有操作將 TransactionScopeRequired 設置為 true 或具有強制事務流。 如果配置操作時將 TransactionScopeRequired 設置為 true,則將使用操作的環境事務來利用存儲。

事務性行為

對於 DurableService 屬性而言,單詞“持久”不太恰當,因為它此時不一定指示持久行為。 它所代表的含義是 WCF 將自動從已配置的存儲中反序列化服務狀態,然後在每次操作時再次將其序列化。 同樣,持久性提供程式列為不一定是指持久性,因為從預定抽象提供程式類衍生的任何提供程式都滿足持久性。

實際上,持久服務基礎結構在現實中是序列化基礎結構,這使我可以在技術中利用這一結構,在出現事務時管理服務狀態,同時在底層依賴不穩定資源管理器,而不必讓服務實例對其做任何事情。 這樣進一步改進了 WCF 的事務性程式設計模型,高級事務程式設計模型在純粹物件和常用服務方面的優勢也得以體現。

第一步是定義兩個事務性記憶體中提供程式工廠:TransactionalMemoryProviderFactory 和 TransactionalInstanceProviderFactory。 TransactionalMemoryProviderFactory 使用靜態 TransactionalDictionary<ID,T> 儲存服務實例。 目錄在所有用戶端和會話之間共用。 只要主機在運行,TransactionalMemoryProviderFactory 就允許用戶端連接服務或從服務斷開連接。 使用 TransactionalMemoryProviderFactory 時,應指定一個完整的操作,以使用 DurableOperation 的 CompletesInstance 屬性從存儲中移除實例狀態。

另一方面,TransactionalInstanceProviderFactory 將每個會話與專用的 Transactional<t> 實例相匹配。 不需要完成操作,因為在關閉會話後服務狀態將做為垃圾被收集起來。

接下來,我定義了 TransactionalBehavior 屬性,如圖 4 所示。 TransactionalBehavior 是執行下列配置的服務行為屬性。 首先,它在 SaveStateInOperationTransaction 設置為 true 時向服務說明中插入一個 DurableService 屬性。 其次,它根據 AutoCompleteInstance 屬性的值,為持久性行為添加使用 TransactionalMemoryProviderFactory 或 TransactionalInstanceProviderFactory 的功能。 如果 AutoCompleteInstance 設置為 true(預設值),它將使用 TransactionalInstanceProviderFactory。 最後,如果 TransactionRequiredAllOperations 屬性設置為 true(預設值),TransactionalBehavior 會在所有服務操作行為上將 TransactionScopeRequired 設置為 true,進而為所有操作提供環境事務。 當它顯式設置為 false 時,服務開發人員可以選擇哪些操作將是事務性的。

圖 4 TransactionalBehavior 屬性

[AttributeUsage(AttributeTargets.Class)]
public class TransactionalBehaviorAttribute : Attribute,IServiceBehavior
{
   public bool TransactionRequiredAllOperations
   {get;set;}

   public bool AutoCompleteInstance
   {get;set;}

   public TransactionalBehaviorAttribute()
   {
      TransactionRequiredAllOperations = true;
      AutoCompleteInstance = true;   
   }
   void IServiceBehavior.Validate(ServiceDescription description,
                                  ServiceHostBase host) 
   {
      DurableServiceAttribute durable = new DurableServiceAttribute();
      durable.SaveStateInOperationTransaction = true;
      description.Behaviors.Add(durable);

      PersistenceProviderFactory factory;
      if(AutoCompleteInstance)
      {
         factory = new TransactionalInstanceProviderFactory();
      }
      else
      {
         factory = new TransactionalMemoryProviderFactory();
      }

      PersistenceProviderBehavior persistenceBehavior = 
                                new PersistenceProviderBehavior(factory);
      description.Behaviors.Add(persistenceBehavior);

      if(TransactionRequiredAllOperations)
      {
         foreach(ServiceEndpoint endpoint in description.Endpoints)
         {
            foreach(OperationDescription operation in endpoint.Contract.Operations)
            {
               OperationBehaviorAttribute operationBehavior =  
                  operation.Behaviors.Find<OperationBehaviorAttribute>();
               operationBehavior.TransactionScopeRequired = true;
            }
         }
      }
   }
   ...
} 

使用 TransactionalBehavior 屬性及預設值時,不要求用戶端進行管理或以任何方式與實例 ID 交互。 對用戶端而言,必須要做的就是通過一種上下文綁定來使用代理,並讓綁定管理實例 ID,如圖 5 所示。 請注意,服務與作為其成員變數的正常整數進行交互。 有趣的是,由於持久行為的緣故,取消實例啟動當然仍與方法邊界上的單調用服務類似,但程式設計模型是常用 .NET 物件的模型。

圖 5 使用 TransactionalBehavior 屬性

[ServiceContract]
interface IMyCounter
{
   [OperationContract]
   [TransactionFlow(TransactionFlowOption.Allowed)]
   void Increment();
}

[Serializable]
[TransactionalBehavior]
class MyService : IMyCounter
{
   int m_Counter = 0;

   public void Increment()
   {
      m_Counter++;
      Trace.WriteLine(m_Counter);
   }
}
//Client side:
MyCounterClient proxy = new MyCounterClient();

using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment();
   scope.Complete();
}    

//This transaction will abort since the scope is not completed 
using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment();
} 

using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment();
   scope.Complete();
}

proxy.Close();

//Traces:
1
2
2

向 IPC 綁定添加上下文

TransactionalBehavior 需要支援上下文協定的綁定。 雖然 WCF 為基本綁定、Web 服務 (WS) 和 TCP 綁定提供上下文支援,但此清單中缺少的是進程間通信(IPC,也稱為管道)綁定。 支援 IPC 綁定非常重要,因為這樣可以通過 IPC 來使用 TransactionalBehavior,從而使熟知的調用從 IPC 中受益。 為此,我定義了 NetNamedPipeContextBinding 類:

public class NetNamedPipeContextBinding : NetNamedPipeBinding
{
   /* Same constructors as NetNamedPipeBinding */

   public ProtectionLevel ContextProtectionLevel
   {get;set;}
}

NetNamedPipeContextBinding 的使用方法與其基類完全相同。 可以像任何其他內置綁定一樣,以程式設計方式使用此綁定。 然而,當在應用程式 .config 檔中使用自訂綁定時,需要通知 WCF 在何處定義自訂綁定。 雖然您可以按每個應用程式來執行此操作,但更簡單的做法是在 machine.config 中引用説明程式類 NetNamedPipeContextBindingCollectionElement,以影響機器上的每個應用程式,如下所示:

<!--In machine.config-->
<bindingExtensions>
   ...
   <add name = "netNamedPipeContextBinding" 
        type = "ServiceModelEx.                NetNamedPipeContextBindingCollectionElement,
                ServiceModelEx"
   />
</bindingExtensions>

也可在工作流程應用程式中使用 NetNamedPipeContextBinding。

圖 6 列出了 NetNamedPipeContextBinding 實現及其支援類中的一段摘錄(在本月的代碼下載中有完整的實現)。 NetNamedPipeContextBinding 的構造函數均將實際構造函數委託給 NetNamedPipeBinding 的基本構造函數,而且它們所做的唯一初始化工作就是設置上下文保護級別,使其預設為 ProtectionLevel.EncryptAndSign。

圖 6 實現 NetNamedPipeContextBinding

public class NetNamedPipeContextBinding : NetNamedPipeBinding
{
   internal const string SectionName = "netNamedPipeContextBinding";

   public ProtectionLevel ContextProtectionLevel
   {get;set;}

   public NetNamedPipeContextBinding()
   {
      ContextProtectionLevel = ProtectionLevel.EncryptAndSign;
   }
   public NetNamedPipeContextBinding(NetNamedPipeSecurityMode securityMode) : 
                                                      base(securityMode)
   {
      ContextProtectionLevel = ProtectionLevel.EncryptAndSign;
   }
   public NetNamedPipeContextBinding(string configurationName)
   {
      ContextProtectionLevel = ProtectionLevel.EncryptAndSign;
      ApplyConfiguration(configurationName);
   }

   public override BindingElementCollection CreateBindingElements()
   {
      BindingElement element = new ContextBindingElement(ContextProtectionLevel,
                            ContextExchangeMechanism.ContextSoapHeader);

      BindingElementCollection elements = base.CreateBindingElements();
      elements.Insert(0,element);

      return elements;
   }

   ... //code excerpted for space
}

任何綁定類的核心均為 CreateBindingElements 方法。 NetNamedPipeContextBinding 訪問其綁定元素的基本綁定集合,並向其添加 ContextBindingElement。 將此元素插入到集合中會添加對上下文協定的支援。

實現的其餘內容僅僅是啟用管理配置的記錄。 ApplyConfiguration 方法由構造函式呼叫,它使用綁定內容的配置名稱。 ApplyConfiguration 使用 ConfigurationManager 類從 .config 檔解析出 netNamedPipeContextBinding,並從中解析出 NetNamedPipeContextBindingElement 的實例。 隨後調用此綁定元素的 ApplyConfiguration 方法,用於配置綁定實例。

NetNamedPipeContextBindingElement 的構造函數會向其配置屬性的基類 Properties 集合中添加上下文保護級別的單一屬性。 在 OnApplyConfiguration(它作為 NetNamedPipeContextBinding.ApplyConfiguration 調用 ApplyConfiguration 的結果而被調用)中,方法首先配置其基本元素,然後根據已配置的級別設置上下文保護級別。

NetNamedPipeContextBindingCollectionElement 類型用於綁定 NetNamedPipeContextBinding 與 NetNamedPipeContextBindingElement。 採用此方式時,如果將 NetNamedPipeContextBindingCollectionElement 添加為綁定擴展,則配置管理器將知道要產生實體哪個類型並為其提供綁定參數。

InProcFactory 和事務

TransactionalBehavior 屬性允許您幾乎可以將應用程式中的每個類都視為事務性類,而不會損害熟悉的 .NET 程式設計模型。 弊端是 WCF 從未被設計為用於非常細化的層級——您將必須創建、打開並關閉多個主機,而且您的應用程式 .config 檔將變得不能使用服務和用戶端分數進行管理。 為解決這些問題,在我編寫的《Programming WCF, 2nd Edition》一書中,我定義了名為 InProcFactory 的類,它讓您可以通過 WCF 來產生實體服務類:

public static class InProcFactory
{
   public static I CreateInstance<S,I>() where I : class
                                         where S : I;
   public static void CloseProxy<I>(I instance) where I : class;
   //More members
}

使用 InProcFactory 時,您在類級別利用 WCF,而無需憑藉顯式管理主機或者具有用戶端或服務 .config 檔。 為使每個類級別均能訪問 TransactionalBehavior 程式設計模型,InProcFactory 類使用 NetNamedPipeContextBinding,同時啟用事務流。 使用圖 5 中的定義,InProcFactory 可啟用圖 7 中的程式設計模型。

圖 7 結合使用 TransactionalBehavior 和 InProcFactory

IMyCounter proxy = InProcFactory.CreateInstance<MyService,IMyCounter>();

using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment();
   scope.Complete();
}    

//This transaction will abort since the scope is not completed 
using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment();
} 
using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment();
   scope.Complete();
}

InProcFactory.CloseProxy(proxy);

//Traces:
Counter = 1
Counter = 2
Counter = 2

圖 7 中的程式設計模型與純 C# 類的程式設計模型相同,沒有任何所有權開銷,另外代碼從事務中完全受益。 我認為這是為將來奠定基礎,未來的趨勢是記憶體本身就是事務性的,而且每個物件都可能是事務性的。

圖 8 顯示的是 InProcFactory 的實現,為了簡潔起見,已刪除了一些代碼。 在每個應用程式定義域,InProcFactory 的靜態構造函數均調用一次,使用 GUID 在每個域中分配一個唯一的新基址。 這使得 InProcFactory 可以在同一台機器上的多個應用程式定義域和進程中多次使用。

圖 8 InProcFactory 類

public static class InProcFactory
{
   struct HostRecord
   {
      public HostRecord(ServiceHost host,string address)
      {
         Host = host;
         Address = new EndpointAddress(address);
      }
      public readonly ServiceHost Host;
      public readonly EndpointAddress Address;
   }
   static readonly Uri BaseAddress = new Uri("net.pipe://localhost/" + 
                                             Guid.NewGuid().ToString());
   static readonly Binding Binding;
   static Dictionary<Type,HostRecord> m_Hosts = new Dictionary<Type,HostRecord>();

   static InProcFactory()
   {
      NetNamedPipeBinding binding = new NetNamedPipeContextBinding();
      binding.TransactionFlow = true;
      Binding = binding;
      AppDomain.CurrentDomain.ProcessExit += delegate
                                             {
                         foreach(HostRecord hostRecord in m_Hosts.Values)
                                                {
                                                 hostRecord.Host.Close();
                                                }
                                             };
   }


public static I CreateInstance<S,I>() where I : class
                                         where S : I
   {
      HostRecord hostRecord = GetHostRecord<S,I>();
      return ChannelFactory<I>.CreateChannel(Binding,hostRecord.Address);
   }
   static HostRecord GetHostRecord<S,I>() where I : class
                                          where S : I
   {
      HostRecord hostRecord;
      if(m_Hosts.ContainsKey(typeof(S)))
      {
         hostRecord = m_Hosts[typeof(S)];
      }
      else
      {
         ServiceHost host = new ServiceHost(typeof(S),BaseAddress);
         string address = BaseAddress.ToString() + Guid.NewGuid().ToString();
         hostRecord = new HostRecord(host,address);
         m_Hosts.Add(typeof(S),hostRecord);
         host.AddServiceEndpoint(typeof(I),Binding,address);
         host.Open();
      }
      return hostRecord;
   }
   public static void CloseProxy<I>(I instance) where I : class
   {
      ICommunicationObject proxy = instance as ICommunicationObject;
      Debug.Assert(proxy != null);
      proxy.Close();
   }
}

InProcFactory 在內部管理將服務類型映射為特定主機實例的字典。 當調用 CreateInstance 創建特定類型的實例時,它使用名為 GetHostRecord 的説明程式方法在目錄中進行查找。 如果字典尚未包含服務類型,此説明程式方法會為其創建一個主機實例,並使用新 GUID 作為唯一的管道名稱向此主機添加一個端點。 CreateInstance 然後從主機記錄獲取該端點的位址,並使用 ChannelFactory<t> 創建代理。

在其靜態構造函數中(該函數在第一次使用類時調用),InProcFactory 訂閱進程退出事件以在進程結束時關閉所有主機。 最後,為了説明用戶端關閉代理,InProcFactory 提供了 CloseProxy 方法,該方法查詢 ICommunicationObject 的代理並將其關閉。 要瞭解如何能夠利用事務性記憶體,請參閱“深入分析”側欄“什麼是事務性記憶體?”。

什麼是事務性記憶體?

您可能已經聽說過事務性記憶體,這是一種用於管理共用資料的新技術,許多人聲稱它可以解決在編寫併發代碼時遇到的所有問題。 或者您聽到的是,事務性記憶體言過其實,只不過是一個研究工具。 真實情況是它屬於兩種極端之間的事物。

事務性記憶體使您可以不必管理單個鎖。 您可以用定義明確的序列塊(在資料庫中稱為工作單元或事務)構建程式。 然後,您可以讓基礎運行時系統、編譯器、硬體或它們的組合提供所需的隔離性和一致性保證。

通常,基礎事務性記憶體系統能夠相當精細地提供最優的併發控制。 事務性記憶體系統不必經常鎖定資源,它假定不存在爭用。 它還檢測這些假定何時不成立,然後回滾在事務期間所做的任何試探性更改。 事務性記憶體系統然後可能會根據實現方式嘗試重新執行代碼塊,直到它能夠在無爭用的情況下完成。 系統仍可以檢測爭用並對其加以管理,不會要求您指定引起的回退或編寫代碼,您也不必重試機制。 如果您有最優的精細併發控制、爭用管理,且無需指定和管理特定的鎖,可以考慮使用那些利用併發的元件,以序列方式解決您的問題。

事務性記憶體承諾提供複合功能,這是現有鎖定機制無法輕鬆實現的。 要將多個操作或多個物件複合在一起,一般需要增加鎖的細微性——通常是將這些操作或物件共同封裝在一個鎖下。 事務性記憶體代表您的代碼自動管理細細微性鎖定,同時又能避免鎖死,保證提供複合功能時不會損害可伸縮性,也不會引入鎖死。

目前,事務性記憶體尚未大規模實現商業化。 使用庫、語言擴展、或編譯器指令的實驗軟體方法已在學術界發表或在網站上發佈。 某些高端、高併發環境中,確實有可以提供有限事務性記憶體的硬體,但利用此硬體的軟體沒有明確指出使用了這種硬體。 事務性記憶體讓研究社區倍感興奮,您可期待此研究的一些成果在未來十年內能夠使其成為更易於使用的產品。

附屬專欄描述了不穩定資源管理器的創建過程,您可以將該管理器與當前事務技術配合使用以提供原子性(或全部執行,或都不執行)、後續可管理性、品質以及事務性程式設計所具有的其他優勢。 事務性記憶體提供相似的功能,但它對於任意資料類型使用相當輕量化的運行時軟體或硬體基元,並側重于提供可伸縮性、隔離性、複合以及原子性,無需您創建自己的資源管理器。 事務性記憶體得以廣泛使用,程式師將不僅能從不穩定資源管理器(如更簡單的程式設計模型)中受益,還會獲得事務性記憶體管理器所帶來的可伸縮性收益。

— Dana Groff 是 Microsoft 平行計算平臺構建團隊的一名高級專案經理

請將您想詢問的問題和提出的意見發送至 mmnet30@microsoft.com

Juval Lowy 是 IDesign 的一名軟體架構師,該公司提供 WCF 培訓和體系結構諮詢。 他最近編寫了一本名為《Programming WCF Services, 2nd Edition》**(O'Reilly, 2008) 的新書。 另外,他還是 Microsoft 矽谷地區的區域總監。 您可以通過 www.idesign. net與 Juval 聯繫。