.NET Framework 2.0 での System.Transactions の概要

 

Juval Lowy

2005 年 12 月

適用対象:
   Microsoft .NET Framework 2.0

概要:.NET Framework 2.0 の新機能である革新的で実用的なアーキテクチャである System.Transactions を使用して、堅牢で高性能なエンタープライズ アプリケーションを構築します。 (35ページ印刷)

内容

はじめに
.NET 1.x トランザクション プログラミング モデル
.NET Framework バージョン 2.0 でのトランザクション管理
System.Transactions の操作
高度なトピック
まとめ

はじめに

Microsoft Windows プラットフォームの開発者は、従来、明示的なトランザクション管理または宣言型トランザクション フローと管理という 2 つのトランザクション プログラミング モデルから選択します。 どちらのプログラミング モデルにも長所と短所があり、いずれのプログラミング モデルも、すべての点で他のモデルよりも優れているとは言えよう。 .NET Frameworkのバージョン 2.0 では、System.Transactions 名前空間で使用できる新しいトランザクション プログラミング モデルが導入されています。 新しいモデルを使用すると、開発者は、手動で作成されたコードの量を最小限に抑え、アプリケーション ホスティング環境とインスタンス管理から分離しながら、可能な限り低いオーバーヘッドでトランザクション コードを簡単に記述できます。 このホワイトペーパーでは、従来のプログラミング モデルに関する問題と、新しいモデルの動機について説明することから始めます。 その後、ホワイトペーパーでは、新しいプログラミング モデル、その機能、および非同期作業、イベント、セキュリティ、コンカレンシー管理、相互運用性などの高度な機能について説明します。

.NET 1.x トランザクション プログラミング モデル

ADO.NET 1.0 では、明示的なトランザクション管理プログラミング モデルが提供されます。 開発者は、例 1 に示すように、トランザクションを明示的に開始および管理する必要があります。

例 1. ADO.NET での明示的なトランザクション管理

string connectionString = "...";
IDbConnection connection = new SqlConnection(connectionString);
connection.Open();
IDbCommand command = new SqlCommand();
command.Connection = connection;

IDbTransaction transaction;
transaction = connection.BeginTransaction(); //Enlisting database
command.Transaction = transaction;
try
{
   /* Interact with database here, then commit the transaction */
   transaction.Commit();
}
catch
{
   transaction.Rollback(); //Abort transaction
}
finally
{
   connection.Close();
}

基になるデータベース トランザクションを表す オブジェクトを取得するには、接続オブジェクトで BeginTransaction() を呼び出します。 BeginTransaction() は 、トランザクションの管理に使用されるインターフェイス IDbTransaction の 実装を返します。 データベースに対して行われたすべての更新またはその他の変更に一貫性がある場合は、トランザクション オブジェクトに対して Commit() を呼び出すだけです。 エラーが発生した場合は、 Rollback() を呼び出してトランザクションを中止する必要があります。

明示的なプログラミング モデルは簡単ですが、図 1 に示すように、単一のデータベース (または単一のトランザクション リソース) と対話する 1 つのオブジェクトに最も適しています。

図 1. 単一オブジェクト/単一リソース トランザクション

明示的なモデルは、複数のオブジェクトまたは複数のリソースを含むトランザクションには特に適していません。 これは、トランザクションの調整によるものです。 たとえば、図 2 に示すように、複数のオブジェクトが相互にやり取りし、データベースなどのリソースと対話するアプリケーションを考えてみましょう。 ここでの質問は、トランザクションを開始してリソースを参加させる役割を担っているオブジェクトの 1 つですか? それらすべてがそれを行う場合は、複数のトランザクションになります。 さらに、トランザクションのコミットまたはロールバックを担当するオブジェクトはどれですか? 1つのオブジェクトは、トランザクションに関する残りのオブジェクトがどのように感じるかを知るでしょうか? トランザクションを管理するオブジェクトは、トランザクションの結果について他のオブジェクトにどのように通知しますか? オブジェクトは、異なるプロセスまたは異なるマシンに展開することもできます。また、ネットワークやマシンのクラッシュなどの問題により、トランザクションを管理するための複雑さが増します。 トランザクション調整のロジックを追加してオブジェクトを結合する 1 つの解決策ですが、このようなアプローチは非常に脆弱であり、ビジネス フローや参加しているオブジェクトの数のわずかな変更にも耐え得ません。 さらに、図 2 のオブジェクトは、このような調整を妨げるさまざまなベンダーによって開発された可能性があります。

図 2. 複数のオブジェクト/1 つのリソース トランザクション

複数のリソースが関係する場合、状況は大幅に複雑になります (図 3 を参照)。

図 3: 複数のリソースにアクセスする複数のオブジェクト

1 つのトランザクションで複数のオブジェクトが関係する課題に加えて、複数のリソースを導入すると、追加の管理要件が導入されます。 複数のリソースにまたがるトランザクションでは、すべてのリソースがトランザクションの代わりに更新プログラムをコミットする必要があるか、いずれも実行しないかのどちらかのセマンティクスを提供する必要があります。 複数の参加者間で更新を調整するには、 分散トランザクションが必要です。

分散トランザクションは、複数のオブジェクトで実行されているアプリケーション コードや、複数のリソース マネージャーによって管理される永続データ、または複数のオブジェクトと複数のリソースの両方を含む更新を調整します。 アプリケーションが分散トランザクションのすべての潜在的なエラー ケースを個別に管理することは現実的ではありません。 分散トランザクションの場合は、専用のトランザクション マネージャーに依存する必要があります。

Windows では、分散トランザクション コーディネーター (DTC) システム サービスによって、アプリケーションにトランザクション管理機能が提供されます。 DTC は、オブジェクトまたはコンポーネント間、プロセスとマシン間、および複数のリソース マネージャー間でトランザクションを管理します。 DTC は 2 フェーズ コミット プロトコルを実装し、任意のプラットフォームで実行されている Oracle データベースや IBM DB2 データベースなどのトランザクション リソースを管理できます。 DTC では、OLE Transactions (OleTx) と呼ばれるプロトコルを使用して Windows ネイティブ トランザクション リソース マネージャーを管理することもできます。このようなリソース マネージャーには、SQL Serverと MSMQ が含まれます。 DTC に対して直接プログラミングすることは可能ですが、.NET Framework バージョン 1.x を使用するアプリケーションでは、DTC トランザクションを利用する最も一般的で簡単な方法は、System.EnterpriseServices 名前空間を介して使用できるエンタープライズ サービスを使用することです。 例 2 は、Enterprise Services トランザクションの使用を示しています。

例 2. Enterprise Services を使用した宣言型トランザクション管理

using System.EnterpriseServices;

[Transaction]
public class MyComponent : ServicedComponent
{
   [AutoComplete]
   public void MyMethod()
   {
      /*Interact with other serviced components
      and resource managers */
   }
}

.NET Enterprise Services には宣言型プログラミング モデルが用意されています。抽象クラス ServicedComponent から派生するすべてのクラスは 、Transaction 属性を使用できます。 属性を使用すると、クラスのいずれかのメソッドが呼び出されると、そのメソッドがトランザクション コンテキスト内で実行されます。 コンテキストは、サービス コンポーネントの最も内側の実行スコープです。 .NET はコンテキストに入ってくる呼び出しをインターセプトし、オブジェクトの代わりにトランザクションを開始します。 アプリケーション コードはトランザクション リソースをトランザクションに明示的に参加させる必要はありません。これは、リソース マネージャーによって自動的に実行されます。 トランザクションに自動的に参加できるリソースは、 トランザクション リソース マネージャーと呼ばれます。これには、一般的な商用データベースと永続リソース (Microsoft Message Queue や IBM MQSeries など) のほとんどが含まれます。

Transaction 属性を使用する ServiceComponent の場合、アプリケーションの要件は最小限です。オブジェクトが行う必要があるのは、トランザクションをコミットまたは中止する必要があるかどうかを .NET に通知することです。 これを明示的に行うか、 ContextUtil ヘルパー クラスのメソッドを使用するか、 AutoComplete メソッド属性を使用して宣言的に行うことができます。 AutoComplete 属性を持つメソッドで例外がスローされない場合、アプリケーションはトランザクションのコミットを暗黙的に要求します。 (トランザクションが 実際に コミットされるかどうかは、トランザクションに関係する他の参加者とリソースによって異なります)。一方、例外が発生した場合、アプリケーションはトランザクションの中止を要求します。 トランザクションのコミット結果には満場一致が必要であるため、これはトランザクションが実際に中止されることを意味します。

宣言型モデルは生産性に大きな利点をもたらしますが、欠陥がないわけではありません。

  • ServiceComponent からの継承を強制すると、通常は内部アプリケーション モデリング用に予約されている基底クラスの貴重な場所が占有されます。
  • Enterprise Services トランザクションの使用は、1 つのリソースと 1 つのオブジェクトが関係している場合でも、分散 DTC トランザクションの使用を常に意味します。 2 フェーズ コミット プロトコルは、トランザクション マネージャー レベルとリソース レベルの両方でコストを意味します。これは、リソースが操作のログ記録を維持する必要があるためです。 オーバーヘッドにより、明示的なトランザクション管理と比較してパフォーマンスが低下する可能性があります。
  • エンタープライズ サービスの使用に関して暗黙的に示されているのは、COM+ ホスティング モデルです。 場合によっては、開発者はこれを不要な結合または不要な複雑さであると見なす場合があります。
  • Enterprise Services トランザクションは、Enterprise Services インスタンス管理戦略と密接に結び付けられます。 すべてのトランザクション オブジェクトも Just-In-Time でアクティブ化され、トランザクションとオブジェクト プールの組み合わせに関してはいくつかの問題があります。 この結合はスケーラブルなアプリケーションでは高く評価されていますが、他のすべてのアプリケーションでは、ほとんどの開発者が困難な状態対応プログラミング モデルを強制します。
  • Enterprise Services トランザクションは常にスレッド セーフです。複数のスレッドが同じトランザクションに参加する方法はありません。 これにより、特にマルチスレッド環境でのトランザクション管理が大幅に簡略化されますが、エッジケースによっては制限があります。

実際には、.NET 1.0 と 1.1 は、非分散トランザクションの使用と明示的なトランザクション管理を同じであり、分散トランザクションの使用と Enterprise Services を介した宣言型トランザクションの使用と同じになります。 DTC トランザクションを使用せずに宣言型トランザクションを使用する方法はありません。また、マネージド コードで DTC を利用する明示的なトランザクション管理を簡単に実行する方法もありません。 プログラミング モデル (明示的または宣言型) を選択すると、トランザクション マネージャーも必ず選択されます。その逆も同様です。

.NET Framework バージョン 2.0 でのトランザクション管理

明示的トランザクション プログラミング モデルと宣言型トランザクション プログラミング モデルの両方で説明した問題に対処するために、.NET Framework バージョン 2.0 では、ライトウェイト トランザクション マネージャー (LTM) と呼ばれる新しい最適化されたトランザクション マネージャーによって補完される、新しい明示的なトランザクション プログラミング モデルが導入されています。 新しいプログラミング モデルでは、分散トランザクションと単一リソース トランザクションに対して明示的なトランザクションの境界が可能になり、.NET v1.1 よりも柔軟性が高くなります。 LTM は、1 つのリソースのみを含むトランザクションに最適化を提供し、可能な場合は高いパフォーマンスを実現します。 トランザクション境界の新しいサポートに加えて、.NET Framework バージョン 2.0 では、トランザクション リソース自体を構築するための新しいサポートも導入されています。

開発者は 、System.Transactions 名前空間内の新しいクラスとインターフェイスを使用して、この機能にアクセスできます。 たとえば、トランザクションを明示的に開始するには、アプリケーションで新しい TransactionScope をインスタンス化できます。 そのトランザクションのアプリケーション コードが 1 つのアプリ ドメイン内で実行され、最大で 1 つの永続リソースが含まれている場合、そのリソースでトランザクションの昇格がサポートされている場合、LTM はトランザクションを調整できます。 複数のアプリ ドメイン (マルチプロセスとマルチマシンのシナリオを含む) で実行されるアプリケーション コードを含むトランザクションの場合、またはすべてのアプリケーション コードが 1 つのアプリ ドメインに存在する場合、または単一のトランザクション リソースが関係していても、リソースがトランザクションの昇格をサポートしていない場合でも、複数の永続リソースを含むトランザクションの場合は、 分散 (OleTx) トランザクション マネージャーが使用されます。 アプリケーション コード自体は、これらの最適化に関心を持つ必要はありません。これらは単に機能します。

この方法でトランザクションで管理できるリソースは、 System.Transactions Resource Managers と呼ばれます。 Enterprise Services の状況と同様に、System.Transactions Resource Manager は、開いているトランザクションに自動的に参加できるリソースです。 通常、リソースはクライアント ライブラリのコードを使用してこれを行い、現在または アンビエント トランザクション スコープを検出します。

単一の共通トランザクション管理名前空間 (System.Transactions) に対するプログラミングを使用すると、アプリケーション コードを変更することなく、トランザクション マネージャーの実装を動的に変更できます。 たとえば、トランザクションの昇格: 最適化されたトランザクション コミット プロトコルを使用できる場合は、(LTM 経由で) 使用されます。 それ以外の場合、トランザクションは、より一般化された分散トランザクション コミット プロトコルに自動的に昇格されます。

たとえば、トランザクションのスコープ内で、1 つのオブジェクトが 2005 SQL Server 1 つと対話するとします。 SQL Serverのインスタンスで実行されるすべての作業は、内部トランザクションSQL Serverによって管理でき、LTM はパススルー レイヤーとしてのみ機能します。 このシナリオでは、最適なスループットとパフォーマンスが提供されます。 アプリケーションが同じコンピューター上の別のアプリ ドメイン内の別のオブジェクトにトランザクションを提供する場合、またはアプリケーションが 2 つ目の永続リソース マネージャーを参加させる場合、トランザクションは自動的に分散トランザクションに昇格され、この場合SQL Server 2005 年に関係する参加者にトランザクションが昇格されたことが通知されます。 昇格されると、分散 2 フェーズ コミット プロトコルが使用されるまで、トランザクションは昇格された状態で管理されたままになります。

別の例として、アプリケーションがトランザクション スコープを開始し、1 つの Oracle データベースと対話するとします。 現在、Oracle ではトランザクションの昇格がサポートされていないため、 System.Transaction ランタイムは分散トランザクションを自動的かつ透過的に使用します。 1 つのオブジェクトと Oracle の 1 つのインスタンスのみを含んだ後でトランザクションが終了した場合でも、Oracle データベース内で内部トランザクション管理を使用できる状況でも、 System.Transaction ランタイムによって分散トランザクションとして管理されます。 これは、 System.Transaction が 将来を予測できないためです。 トランザクション マネージャーは、そのリソース マネージャーで作業を実行する前に、永続リソース マネージャーでトランザクションを開く必要があります。 Oracle での最初の操作の時点で、 System.Transaction ランタイムは、後で他のリソース マネージャーがトランザクションに関与しないことを確認できません。 したがって、Oracle での初期作業には分散トランザクションを使用する必要があります。 Oracle データベースで昇格のサポートが提供されている場合は、SQL Server 2005 と同様に LTM 最適化を使用できます。

System.Transactions 名前空間の基本クラスは Transaction ですトランザクション は、トランザクションにリソースを参加させ、トランザクションを中止し、分離レベルを設定し、トランザクションの状態と ID を取得し、トランザクションを複製するために使用されます。 トランザクションをコミットするために、 System.Transactions は CommittableTransaction クラスを定義します。

public interface ITransaction : IDisposable
{
   void Rollback();
}
[Serializable]
public class Transaction : ITransaction,ISerializable
{
   public void Rollback(); //Abort the transaction
   public static Transaction Current{get;set;}
   //Other members
}
[Serializable]
public sealed class CommittableTransaction : Transaction,IAsyncResult
{
   public void Commit();
   //Other members
}

1 つではなく 2 つのクラスの理由については、後で説明します。

System.Transactions を使用する場合、アプリケーションでは、MSMQ を処理するときに、T-SQL BEGIN TRANSACTION 動詞や COMMIT TRANSACTION 動詞、System.Messaging 名前空間の MessageQueueTransaction() オブジェクトなど、リソース マネージャー上のトランザクション プログラミング インターフェイスを直接使用しないでください。 これらのメカニズムでは、 System.Transactions によって処理される分散トランザクション管理がバイパスされ、 System.Transactions の使用とこれらのリソース マネージャーの "内部" トランザクションを組み合わせると、一貫性のない結果が得られます。 原則として、一般的なケースでは System.Transactions を 使用し、リソース マネージャーの内部トランザクションは、トランザクションが複数のリソースにまたがるのではなく、より大きなトランザクションに構成されないことが確実な特定の場合にのみ使用します。 2 つを混ぜない。

System.Transactions は、 アンビエント トランザクションと呼ばれる概念を定義します。 アンビエント トランザクションは、現在のアプリケーション コードが実行しているスレッド内に存在するトランザクションです。 アンビエント トランザクションへの参照を取得するには、Transaction の静的 Current プロパティを呼び出します。

Transaction ambientTransaction = Transaction.Current;

アンビエント トランザクションがない場合、 Currentnull を返します

アンビエント トランザクション オブジェクトは、スレッド ローカル ストレージ (TLS) に格納されます。 その結果、スレッドが複数のオブジェクトとメソッドにまたがって進むと、すべてのオブジェクトとメソッドがアンビエント トランザクションにアクセスできます。

トランザクションの昇格

特に指定しない限り、.NET 2.0 のすべての System.Transactions トランザクションは LTM によって管理されるトランザクションとして開始されます。 最大で 1 つの永続リソース マネージャーが関係している限り、基になるリソース (Microsoft SQL Server 2005 など) でトランザクションを管理させることに問題はありません。 このような場合、LTM はトランザクションを実際に管理する必要はありません。昇格の必要性を監視するために、その役割を減らす必要があります。 これは LTM が行うとおりです。これは、基になるリソースのアダプターとして機能し、そのマネージド トランザクションの呼び出しを基になるリソースの呼び出しに変換します。 一方、昇格が必要な場合、LTM は、トランザクションに対する唯一の制御を放棄するようにリソース マネージャーに通知する必要があります。 LTM は次に、リソース マネージャーに分散トランザクション識別子を提供し、リソース マネージャーは、これまでに実行された作業について、そのトランザクションに代わって実行された将来の作業について、トランザクションのコミットまたはロールバックが外部分散トランザクション コーディネーターによって調整されることを理解します。 この相互作用をサポートするには、リソース マネージャーで、次のように定義されたインターフェイス IPromotableSinglePhaseNotification を実装する 必要があります。

public interface IPromotableSinglePhaseNotification
{
   void Initialize();
   Transaction Promote();
   void Rollback(SinglePhaseEnlistment singlePhaseEnlistment);
   void SinglePhaseCommit(SinglePhaseEnlistment 8 singlePhaseEnlistment);
}

LTM トランザクションがリソース マネージャーにアクセスすると、リソース マネージャー クライアント ライブラリはアンビエント トランザクションを検出し、それ自体をトランザクションに参加させます。 LTM は、リソース マネージャーに IPromotableSinglePhaseNotification の実装を照会します。 LTM のトランザクションをコミットまたは中止するための呼び出しは、それぞれ IPromotableSinglePhaseNotification (SinglePhaseCommit()Rollback() の呼び出しに変換されます。

ここでのキャッチは、リソース マネージャーが昇格をサポートしている場合にのみ LTM を使用できることです。 トランザクションのスコープ内で、アプリケーションが Oracle データベースとのみ対話するシナリオを考えてみましょう。 これは、1 人の参加者による最適化に適した候補です。 ただし、トランザクション マネージャー (System.Transactions) には、アプリケーションにトランザクションに他の永続リソースも含まれないことを 事前 に知る方法はありません。 このため、LTM を使用できるのは、 System.Transactions が単一の永続リソースが必要に応じて、トランザクションの有効期間中のある時点で昇格をサポートできる場合にのみ使用できます。

LTM オーバーヘッドは非常に軽量です。 Microsoft が 2005 年SQL Server使用したパフォーマンス ベンチマークでは、LTM トランザクションの使用とネイティブ トランザクションの使用を比較すると、2 つの方法の使用に統計的な違いは見つかりませんでした。

System.Transactions を使用すると、リソース マネージャーの開発者は、先ほど説明した操作を簡単にサポートできます。 トランザクションへの参照を取得するために、リソース マネージャーは Transaction の静的 Current プロパティを呼び出 します。 トランザクションに参加するために、リソース マネージャーは、永続リソースに対して Transaction オブジェクトの EnlistDurable() メソッドを呼び出すか、揮発性リソース (メモリにのみ状態を格納するリソース) の EnlistVolatile() メソッドを呼び出します。

[Serializable]
public class Transaction : ITransaction,ISerializable
{
   public Enlistment EnlistDurable(...);
   public Enlistment EnlistVolatile(...);
   //Other members
}

LTM は、トランザクションを分散トランザクションに昇格することを決定すると、単に IPromotableSinglePhaseNotificationPromote() メソッドを呼び出します。 内部的には、リソース マネージャーはトランザクションをローカル トランザクションから分散トランザクションに変換します。 正味の効果は、分散トランザクションの最初からリソースが参加したかのように表示されます。

IPromotableSinglePhaseNotification の実装は昇格可能なトランザクションに参加するための鍵であるため、SQL Server 2000、MSMQ、Oracle Database、IBM DB2 などのトランザクション リソース マネージャーの現在のバージョンは LTM トランザクションに参加できません。 このようなリソースが LTM トランザクションによってアクセスされると、そのようなリソースが 1 つだけ関係していても、トランザクションは自動的に分散トランザクションに昇格されます。 ここでも、これはアプリケーション開発者には透過的ですが、前述のようにパフォーマンス コストがあります。 Microsoft はベンダーと協力して、リソース マネージャーの今後のリリースで LTM 最適化のサポートを奨励しています。

昇格のトリガー

前述のように、.NET 2.0 のすべての System.Transactions トランザクションは、LTM によって管理される軽量トランザクションとして開始されます。 昇格をトリガーするイベントには 2 種類があります。1 つ目は、2 番目の永続リソース マネージャーのトランザクションへの参加です。 たとえば、アプリケーションがトランザクションを開始し、SQL Server データベースへの接続を開くとします。 トランザクションは LTM によって管理されます。 その後、アプリケーションが Oracle データベース上で接続を開き、Oracle が開いているトランザクションに参加したとします。 この時点で、LTM はトランザクションに関係する複数の永続リソースを検出し、トランザクションを分散トランザクションに昇格させます。

2 つ目の昇格トリガーは、アプリ ドメイン境界を越えたトランザクション オブジェクトのシリアル化と転送です。 トランザクション は値別にマーシャリングされます。つまり、アプリ ドメイン境界を越えて渡そうとすると (同じプロセスであっても) トランザクション オブジェクトがシリアル化されます。

アプリケーションはトランザクション オブジェクトを明示的に渡し、 Transaction をパラメーターとして受け取るリモート メソッドを呼び出すことができます。 アプリケーションは、リモート トランザクション の ServiceComponent にアクセスすることで、トランザクションを暗黙的にシリアル化および送信することもできます。 トランザクションのシリアル化は、アプリ ドメインを介してシリアル化されるときに、実際にはトランザクションを分散しており、LTM (パススルーとして機能する) が適切ではなくなったため、トランザクションを昇格することを意味します。 LTM トランザクション クラスでは、カスタム シリアル化が使用されます。 シリアル化要求の処理では、トランザクションが昇格され、逆シリアル化要求の処理では、新しい OleTx トランザクションの新しいアプリ ドメインに参加します。

System.Transactions の操作

前述のように、 System.Transactions は アプリケーション プログラミング モデルをトランザクション マネージャーから切り離します。 アプリケーションでは、 System.Transactions 名前空間のクラスを使用して、トランザクションを明示的に管理できます。 アプリケーションは、System.EnterpriseServices のクラスに依存して宣言型トランザクションを使用することもできます。

宣言型プログラミング モデル

.NET v1.x の System.EnterpriseServices からの宣言型トランザクション プログラミング モデルは、.NET v2.0 では変更されません。 良いニュースは、 ServiceComponentSystem.Transactions と LTM に関連するパフォーマンスの最適化を、コードを変更なしで利用することです。 例 2 に示す Enterprise Services コードは、.NET 2.0 でコンパイルおよび実行された場合、可能であれば LTM を自動的に使用し、必要に応じてより一般的な分散トランザクション プロトコルを使用します。 これにより、可能な限りパフォーマンスの最適化を提供しながら、Enterprise Services の最も重要な生産性とアプリケーション モデリングの利点が維持されます。 さらに、この変更は、アプリケーション ロジック自体から切り離されているため、既存のアプリケーションには影響しません。

明示的なプログラミング モデル

.NET Enterprise Services を介した宣言型トランザクションの代わりに、 System.Transactions を使用した明示的なトランザクション管理があります。 これを行う最も一般的で簡単な方法は、 TransactionScope クラスを利用することです。

public class TransactionScope : IDisposable
{
   public void Complete();
   public void Dispose();
   public TransactionScope();
   public TransactionScope(Transaction transactionToUse);
   public TransactionScope(TransactionScopeOption scopeOption);
   public TransactionScope(TransactionScopeOption scopeOption, 
   public TransactionScope(TransactionScopeOption scopeOption, 
TimeSpan scopeTimeout);
   //Additional constructors
}

名前が示すように、 TransactionScope クラスを使用してトランザクション スコープを作成および管理します (例 3 を参照)。 内部的には、TransactionScope オブジェクトはトランザクション (既定では LTM) を作成し、Transaction クラスの Current プロパティを設定してアンビエント トランザクションとして割り当てます。 TransactionScope は破棄可能なオブジェクトです。 Dispose() メソッドが呼び出されると、トランザクションは終了します (例 3 の using ステートメントの末尾)。

例 3. TransactionScope クラスの使用。

using(TransactionScope scope = new TransactionScope())
{
   /* Perform transactional work here */
   //No errors-commit transaction
   scope.Complete();
}

Dispose() メソッドは、アンビエント トランザクションも元の状態に復元します。例 3 の場合は null です。

トランザクション スコープ (通常は using ステートメント内) 内のコードが完了するまでに長い時間がかかる場合は、トランザクション デッドロックを示している可能性があります。 この問題に対処するために、事前に構成されたタイムアウトを超えて実行された場合、トランザクションは自動的に中止されます。 既定のタイムアウトは 60 秒ですが、アプリケーションは TransactionScope の代替コンストラクターのいずれかを使用してタイムアウトを指定できます。 アプリケーション管理者は、既定のタイムアウトを (既定では 60 秒) 変更することもできます。 タイムアウトは、構成ファイルを介してプログラムと管理の両方で構成できます。

最後に、 TransactionScope オブジェクトが using ステートメント内で使用されていない場合、トランザクションタイムアウトが切れてトランザクションが中止されると、そのオブジェクトはガベージになります。

TransactionScope オブジェクトには、トランザクションをコミットまたは中止する必要があるかどうかを知る方法はありませんが、TransactionScope のメイン目的は、開発者がトランザクションを直接操作する必要性を防することです。 これに対処するために、すべての TransactionScope オブジェクトには 整合性 ビットがあり、既定では false に設定 されています整合性ビットを true に設定するには、Complete() メソッドを呼び出します。 Complete() を呼び出すことができるのは 1 回だけであることに注意してください。 Complete () の後続の呼び出しでは、 InvalidOperation 例外が発生します。 これは、 Complete() の呼び出し後に、開発者がトランザクション コードを持たないことを奨励するために意図的です。

( Dispose() またはガベージ コレクションの呼び出しにより) トランザクションが終了し、 整合性 ビットが false に設定されている場合、トランザクションは中止されます。 たとえば、整合性ビットが既定値から変更されることはないため、次のスコープ オブジェクトは常にそのトランザクションをロールバックします。

using(TransactionScope scope = new TransactionScope())
{
}

一方、 Complete() を呼び出し、トランザクションが終了し、例 3 のように整合性ビットが true に設定されている場合、トランザクションはコミットされ、すべてのトランザクション リソース マネージャーがコミットできると仮定します。 Complete() を呼び出した後、アンビエント トランザクションにアクセスできないため、これを試みると無効な操作例外が発生します。 (Complete() を呼び出した) スコープ オブジェクトが破棄されたら、アンビエント トランザクション (Transaction.Current 経由) に再度アクセスできます。 アプリケーションが Complete() を呼び出した場合でも、トランザクションが中止される可能性があることに注意してください。 この場合、 Dispose()TransactionAbortedException をスローします。 例 4 に示すように、その例外を処理できます。

例 4. TransactionScope とエラー処理

try
{
   using(TransactionScope scope = new TransactionScope())
   {
      /* Perform transactional work here */
      //No errors-commit transaction
      scope.Complete();
   }
}
catch(TransactionAbortedException e)
{
   // handle abort condition
}
catch //Any other exception took place
{
   Trace.WriteLine("Cannot complete transaction");
   throw;
}

トランザクション フロー管理

トランザクション スコープは、直接および間接的に入れ子にすることができます。 直接スコープの入れ子は、例 5 に示すように、1 つのスコープを別のスコープ内に入れ子にするだけです。

例 5. 直接スコープの入れ子

using(TransactionScope scope1 = new TransactionScope())
{
   using(TransactionScope scope2 = new TransactionScope())
   {
      scope2.Complete();
   }
   scope1.Complete();
}

間接スコープの入れ子は、例 6 の RootMethod() の場合と同様に、独自のスコープを使用するメソッド内から TransactionScope を使用するメソッドを呼び出すときに発生します。

例 6. 間接スコープの入れ子

void RootMethod()
{
   using(TransactionScope scope = new TransactionScope())
   {
      /* Perform transactional work here */
      SomeMethod();
      scope.Complete();
   }
}

void SomeMethod()
{
   using(TransactionScope scope = new TransactionScope())
   {
      /* Perform transactional work here */
      scope.Complete();
   }
}

直接入れ子と間接入れ子の両方を含む複数のスコープ入れ子を使用できます。 最上位のスコープは 、ルート スコープと呼ばれます。 もちろん、ルートスコープとすべての入れ子になったスコープの関係は何ですか? スコープの入れ子はアンビエント トランザクションにどのように影響しますか? すべてのスコープは同じトランザクションに参加しますか? 入れ子になったスコープ内のトランザクションに対する投票は、その包含スコープに影響しますか?

オプションがあります。 TransactionScope クラスには、次のように定義された TransactionScopeOption 型の列挙体を受け入れるオーバーロードされたコンストラクターがいくつか用意されています。

public enum TransactionScopeOption
{
   Required,
   RequiresNew,
   Suppress
}

TransactionScopeOption の値を使用すると、スコープがトランザクションに参加するかどうかと、そのスコープがアンビエント トランザクションに参加するか、新しいトランザクションのルート スコープになるかを制御できます。

スコープ オプションの既定値は TransactionScopeOption.Required です。 これは、 TransactionScopeOption パラメーターを受け取らないコンストラクターの 1 つを使用するときに使用される値です。 アプリケーションがスコープのコンストラクターで TransactionScopeOption の値を明示的に指定する方法を次に示します。

using (TransactionScope scope = new 
TransactionScope(TransactionScopeOption.Required))
{...}
.

TransactionScope オブジェクトは、構築時に属するトランザクションを決定します。 決定されると、スコープは常にそのトランザクションに属します。 TransactionScope は、アンビエント トランザクションが存在するかどうかと TransactionScopeOption パラメーターの値という 2 つの要因に基づいて決定されます。

TransactionScope オブジェクトには、次の 3 つのオプションがあります。

  • アンビエント トランザクションを結合します。
  • 新しいスコープ ルート、つまり、新しいトランザクションを開始し、そのトランザクションを独自のスコープ内の新しいアンビエント トランザクションにします。
  • トランザクションにまったく参加しないでください。

スコープが TransactionScopeOption.Required で構成されていて、アンビエント トランザクションが存在する場合、スコープはそのトランザクションに参加します。 一方、アンビエント トランザクションがない場合、スコープは新しいトランザクションを作成し、ルート スコープになります。

スコープが TransactionScopeOption.RequiresNew で構成されている場合は、常にルート スコープになります。 新しいトランザクションが開始され、そのトランザクションがスコープ内の新しいアンビエント トランザクションになります。

スコープが TransactionScopeOption.Suppress で構成されている場合、アンビエント トランザクションが存在するかどうかに関係なく、トランザクションの一部になることはありません。 TransactionScopeOption.Suppress で構成されたスコープは、アンビエント トランザクションとして常に null になります

TransactionScopeOption 割り当て決定真理値テーブルを表 1 に要約します。

表 1 TransactionScopeOption デシジョン トゥルース テーブル

TransactionScopeOption アンビエント トランザクション スコープは に参加します
必須 いいえ 新規トランザクション (ルートになる)
RequiresNew いいえ 新規トランザクション (ルートになる)
Suppress いいえ トランザクションなし
必須 はい アンビエント トランザクション
RequiresNew はい 新規トランザクション (ルートになる)
Suppress はい トランザクションなし

TransactionScope オブジェクトが既存のアンビエント トランザクションに参加する場合、スコープ オブジェクトを破棄してもトランザクションは終了しません。 アンビエント トランザクションがルート スコープによって作成された場合、ルート スコープが破棄された場合にのみトランザクションが終了します。 アンビエント トランザクションが手動で作成された場合は、作成者によってコミットまたは中止されたとき、またはタイムアウトしたときに終了します。

例 7 は、別の TransactionScopeOption 値で構成された他の 3 つのスコープ オブジェクトを作成する TransactionScope オブジェクトを示し、図 4 は結果のトランザクションをグラフィカルに示しています。

例 7. スコープ間のトランザクション フロー

using(TransactionScope scope1 = new TransactionScope())
//Default is Required
{
   using(TransactionScope scope2 = new 
TransactionScope(TransactionScopeOption.Required))
   {...}

   using(TransactionScope scope3 = new 
TransactionScope(TransactionScopeOption.RequiresNew))
   {...}

   using(TransactionScope scope4 = new 
TransactionScope(TransactionScopeOption.Suppress))
   {...}

   ...
}

図 4: トランザクション スコープ間のトランザクション フロー

例 7 に、TransactionScopeOption.Required を使用して新しい TransactionScope (scope1) を作成するアンビエント トランザクションを含まないコード ブロックを一覧表示します。 scope1 はルート スコープになります。新しいトランザクション (トランザクション A) を作成し、 トランザクション A をアンビエント トランザクションにします。 その後、scope1 は、それぞれ異なる TransactionScopeOption 値を持つ 3 つのオブジェクトをさらに作成します。 たとえば、 scope2 は必要なサポート用に構成されており、アンビエント トランザクションがあるため、 トランザクション A に参加します。 scope3 は新しいトランザクションである トランザクション B のルート スコープであり、 scope4 にはトランザクションがないことに注意してください。

TransactionScopeOption の詳細

TransactionScopeOption の既定値と最も一般的に使用される値は TransactionScopeOption.Required ですが、その他の各値は使用されます。

TransactionScopeOption.Suppress は、コード セクションによって実行される操作が必要で、操作が失敗した場合にアンビエント トランザクションを中止しない場合に便利です。 TransactionScopeOption.Suppress を使用すると、例 8 に示すように、トランザクション スコープ内に非トランスナショナル コード セクションを作成できます。

例 8. TransactionScopeOption.Suppress の使用

using(TransactionScope scope1 = new TransactionScope())
{
   try
   {
      //Start of non-transactional section
      using(TransactionScope scope2 = new 
 TransactionScope(TransactionScopeOption.Suppress))
      {
         //Do non-transactional work here
      }
      //Restores ambient transaction here
   }
   catch
   {}
   //Rest of scope1
}

TransactionScopeOption.Suppress が役立つもう 1 つの例は、カスタム動作を指定する場合、独自のプログラムによるトランザクション サポートを実行するか、リソースを手動で参加させる必要がある場合です。

つまり、トランザクション スコープと非トランザクション スコープを混在させると、分離と一貫性が損なわれる可能性があるため、注意が必要です。 非トランザクション スコープにはエラーがあり、トランザクションの結果に影響を与えない (一貫性を脅かす) か、まだコミットされていない情報 (分離を脅かす) に基づいて動作する可能性があります。

TransactionScopeOption.Required を使用する場合、TransactionScope 内のコードがルートであるか、アンビエント トランザクションに参加している場合に、動作が異なってはなりません。 いずれの場合にもスコープ内のコードは同じ動作をします。 コードで違いを見分ける方法はありません。 スコープを TransactionScopeOption.RequiresNew に構成すると、アンビエント トランザクションのスコープ外でトランザクション処理を実行する場合に便利です。たとえば、ログ記録または監査操作を実行する場合や、アンビエント トランザクションがコミットまたは中止されるかどうかに関係なく、サブスクライバーにイベントを発行する場合などです。

TransactionScopeOption.RequiresNew 値を使用する場合は非常に注意し、2 つのトランザクション (アンビエント トランザクションとスコープ用に作成されたトランザクション) で、一方が中止され、もう一方のコミットが中止されても不整合が生じないことを確認する必要があります。

入れ子になったスコープ内での投票

入れ子になったスコープは親スコープのアンビエント トランザクションに参加できますが、2 つのスコープ オブジェクトには 2 つの異なる整合性ビットが含まれる点に注意することが重要です。 入れ子になったスコープで Complete() を呼び出しても、親スコープには影響しません。

using(TransactionScope scope1 = new TransactionScope())
{
   using(TransactionScope scope2 = new TransactionScope())
   {
      scope2.Complete();
   }
   //Consistency bit of scope1 is still false
}

ルート スコープから最後の入れ子になったスコープまでのすべてのスコープがトランザクションのコミットに投票した場合にのみ、トランザクションがコミットされます。

TransactionScope タイムアウトの設定

TransactionScope のオーバーロードされたコンストラクターの中には、トランザクションのタイムアウトを制御するために使用される TimeSpan 型の値を受け取るものもあります。次に例を示します。

public TransactionScope(TransactionScopeOption scopeOption, TimeSpan  scopeTimeout);

既定値の 60 秒とは異なるタイムアウトを指定するには、目的の値を渡します。

TimeSpan timeout = TimeSpan.FromSeconds(30);
using(TransactionScope scope = new 8 TransactionScope(TransactionScopeOption.Required, timeout))
{...}

タイムアウトをゼロに設定すると、タイムアウトは無期限になります。 無限タイムアウトは、主にデバッグに役立ちます。コードをステップ実行してビジネス ロジックの問題を分離しようとしたときに、問題を把握している間にデバッグするトランザクションをタイムアウトにしたくない場合です。 トランザクション デッドロックに対するセーフガードがないことを意味するため、他のすべての場合は無限タイムアウトに非常に注意してください。

2 つのケースでは、通常 、TransactionScope タイムアウトを既定値以外の値に設定します。 第 1 は、開発時に、中止されたトランザクションをアプリケーションがどう処理するかをテストする場合です。 TransactionScope タイムアウトを小さい値 (1 ミリ秒など) に設定すると、トランザクションが失敗し、エラー処理コードが観察されます。 TransactionScope トランザクション タイムアウトを既定のタイムアウトより小さく設定する 2 番目のケースは、開いているトランザクションの代わりにロックを保持する期間をより厳密に制限する場合です。 高度に同時実行されるシステムでは、アプリケーション アーキテクトは、ロックの競合を最小限に抑えるために、5 秒を超える 1 つのトランザクションを開かないことを指定できます。

入れ子になった TransactionScope 階層では、タイムアウトはすべてのタイムアウトの和集合です。 実際には、階層内のすべてのスコープの最小タイムアウトが優先されます。

TransactionScope 分離レベルの設定

TransactionScope のオーバーロードされたコンストラクターの一部は、次のように定義された TransactionOptions 型の構造体を受け入れます。

public struct TransactionOptions
{
   public IsolationLevel IsolationLevel{get;set;}
   public TimeSpan Timeout{get; set;}
   //Other members
}

TransactionOptionsTimeout プロパティを使用してタイムアウトを指定できますが、TransactionOptions に使用メインは分離レベルを指定するためです。 既定では、トランザクションは分離レベルを serializable に設定して実行されます。 ただし、 TransactionOptions IsolationLevel プロパティには、次のように定義された列挙型 IsolationLevel の値を割り当てることができます。

例 9. 分離レベルの指定

public enum IsolationLevel
{
   ReadUncommitted,
   ReadCommitted,
   RepeatableRead,
   Serializable,
   Unspecified,
   Chaos,   //No isolation whatsoever
   Snapshot //Special form of read committed 8 
supported by SQL Server 2005
}

例 9 は、分離レベルを指定する方法を示しています。

TransactionOptions options = new TransactionOptions();
options.IsolationLevel = IsolationLevel.ReadCommitted;
options.Timeout = TransactionManager.DefaultTimeout;

using(TransactionScope scope = new 8 TransactionScope(TransactionScopeOption.Required, options))
{...}

シリアル化以外の分離レベルの選択は、読み取り集中型システムでよく使用されます。トランザクション処理の理論とトランザクション自体のセマンティクス、関連するコンカレンシーの問題、およびシステム整合性の結果をしっかりと理解する必要があります。 さらに、すべてのリソース マネージャーがすべての分離レベルをサポートするわけではなく、構成されたレベルより高いレベルのトランザクションへの参加をリソース マネージャーが選択する場合もあります。 Serialized 以外のすべての分離レベルは、他のトランザクションが同じ情報にアクセスした結果、何らかの不整合の影響を受ける可能性があります。 4 つの分離レベルの違いは、異なるレベルが読み取りロックと書き込みロックを使用する方法です。 ロックには、トランザクションがリソース マネージャーのデータにアクセスするときのみ保持できるか、トランザクションがコミットまたは中止されるまで保持できるかの 2 種類があります。 前者はスループットの面で優れており、後者は整合性の面で優れています。 2 種類のロックと 2 種類の操作 (読み取り/書き込み) から、4 つの基本分離レベルが構成されます。 分離レベルの包括的な説明については、トランザクション処理の教科書を参照してください。

入れ子になった TransactionScope オブジェクトを使用する場合、アンビエント トランザクションに参加させる場合は、入れ子になったすべてのスコープがまったく同じ分離レベルを使用するように構成する必要があります。 入れ子になった TransactionScope オブジェクトがアンビエント トランザクションに参加しようとしても、別の分離レベルを指定すると、 ArgumentException がスローされます。

TransactionScope の利点

TransactionScope オブジェクトは、例 1 のプログラミング モデルと比較して明確な利点と利点を提供します。

  • トランザクション スコープ内のコードはトランザクションであるだけでなく、昇格可能です。 トランザクションは LTM で始まり、 System.Transactions はリソースまたはリモート オブジェクトとの対話の性質に従って、必要に応じて昇格させます。
  • スコープはアプリケーション オブジェクト モデルに依存しないため、コードのどの部分でも TransactionScope を使用でき、トランザクションになります。 特別な基底クラスや属性は必要ありません。
  • トランザクションにリソースを明示的に参加させる必要はありません。 System.Transactions リソース マネージャーは、スコープによって作成されたアンビエント トランザクションを検出し、自動的に参加します。
  • 全体的には、トランザクション フローと入れ子を含むより複雑なシナリオでも、シンプルで直感的なプログラミング モデルです。

高度なトピック

TransactionScope クラスは、基になる System.Transactions トランザクション マネージャーとコミット可能なトランザクション オブジェクトをラップします。 TransactionScope クラスの使用に関してこれまでに見てきたものはすべて、直接行うことができます。 この点を例 10 に示します。

例 10. トランザクションを手動で要求する

Transaction oldAmbient = Transaction.Current;
CommittableTransaction committableTransaction;
committableTransaction = oldAmbient as CommittableTransaction;
if(committableTransaction == null)
{
   committableTransaction = new CommittableTransaction();
   Transaction.Current = committableTransaction;
}

try
{
   /* Perform transactional work here */
   //No errors-commit transaction
   committableTransaction.Commit();
}
finally
{
   committableTransaction.Dispose();
   //Restore the ambient transaction
   Transaction.Current = oldAmbient;
}

例 10 は、TransactionScopeOption.Required に設定された TransactionScope の使用例 3 に相当します。 例 10 は、Transaction クラスの静的プロパティ Current を使用してアンビエント トランザクション オブジェクトへの参照を取得することから始めます。 CurrentTransaction 型であり、 Transaction には多くの重要なメソッドが含まれていますが、 Commit() メソッドはありません。

これは仕様上、トランザクション オブジェクト (またはその複製) を他のパーティ (他のスレッド上にある可能性があります) に渡し、トランザクションに投票させますが、トランザクションを自分自身にコミットする行為を予約します。 トランザクションをコミットするには、既に説明されている CommittableTransaction を使用する必要があります。 CommittableTransaction は、例 11 に示されています。

例 11。 CommittableTransaction クラス

 [Serializable]
public sealed class CommittableTransaction : 
Transaction,IAsyncResult
{
   public CommittableTransaction();
   public CommittableTransaction(TimeSpan timeout);
   public CommittableTransaction(TransactionOptions 
transactionOptions);

   public void Commit();
   public IAsyncResult BeginCommit(AsyncCallback 
asyncCallback,object asyncState);
   public void EndCommit(IAsyncResult asyncResult);
}

CommittableTransaction をインスタンス化しても、アンビエント トランザクションは設定されません。 新しく作成されたトランザクションをアンビエント トランザクションにする場合、アプリケーションは Transaction.Current に割り当てる必要があります。

Transaction.Current = committableTransaction;

Transaction.Current に明示的に割り当てる場合は、以前の値を保存し、アプリケーションが CommittableTransaction オブジェクトを使用して終了したときに復元する必要があります。 多くの場合、これは finally 句内で行うことができます。

例 10 は TransactionScope の実際の動作を解明するのに役立ちますが、 CommittableTransaction (非同期コミット) には興味深い用途があります。 トランザクションのコミットには、複数のデータベース アクセス、ネットワーク待機時間などが含まれるため、しばらく時間がかかる可能性があります。 トランザクションの実行時間 (特にデッドロックを解決する場合) には注意が必要ですが、高スループットアプリケーションでは、できるだけ早く国境を越えた作業を完了し、実際のコミット自体をバックグラウンドで実行することができます。 CommittableTransactionの BeginCommit() メソッドと EndCommit() メソッドは、このような目的を果たします。 これらのメソッドは、例 12 に示すように、.NET での非同期呼び出しの他の場合と同様に使用します。 BeginCommit() を呼び出すと、コミットの保留がスレッド プールのスレッドにディスパッチされます。

正しく行うには、アプリケーションで EndCommit() を呼び出す必要があります。 確かに、トランザクションの結果を判断するには、アプリで EndCommit() を呼び出す必要があります。 ただし、これは非同期デリゲートであるため、正しくするために、アプリケーションは結果を気にしない場合でも EndCommit() を呼び出す必要があります。 トランザクションが (何らかの理由で) コミットに失敗した場合、 EndCommit() はトランザクション例外を発生させます。 EndCommit() は、トランザクションがコミット (または中止) されるまで呼び出し元をブロックします。 コミットを非同期的に呼び出す最も簡単な方法は、コミットが完了したときに呼び出されるコールバック メソッドを提供することです。 呼び出しの呼び出しに使用される元のコミット可能なトランザクション オブジェクトで EndCommit() を呼び出す必要があります。 幸いなことに、CommittableTransactionIAsyncResult から派生し、同じオブジェクトであるため、コールバック メソッドの IAsyncResult パラメーターをダウンケースで簡単に取得できます。 また、コミットが完了するまで、トランザクションはリソース マネージャーのロックを (分離レベルに従って) 維持します。

例 12. トランザクションを非同期的にコミットする

public void DoTransactionalWork()
{
   Transaction oldAmbient = Transaction.Current;
   CommittableTransaction committableTransaction = new 
CommittableTransaction();
   Transaction.Current = committableTransaction;

   try
   {
      /* Perform transactional work here */
      //No errors-commit transaction asynchronously.
      committableTransaction.BeginCommit(OnCommitted,null);
   }
   finally
   {
      //Restore the ambient transaction
      Transaction.Current = oldAmbient;
   }
}
void OnCommitted(IAsyncResult asyncResult)
{
   CommittableTransaction committableTransaction;
   committableTransaction = asyncResult as CommittableTransaction;
   Debug.Assert(committableTransaction != null);
   try
   {
      using(committableTransaction)
      {
          committableTransaction.EndCommit(asyncResult);
      }
   }
   catch(TransactionException e)
   {
      //Handle the failure to commit
   }
}

トランザクション イベント

Transaction クラスは、次のように定義 された TransactionCompleted というパブリック イベントを提供します。

public delegate void TransactionCompletedEventHandler(object sender,
TransactionEventArgs e);
public class TransactionEventArgs: EventArgs
{
   public TransactionEventArgs();
   public Transaction Transaction{get;}
}
[Serializable]
public class Transaction : ITransaction,ISerializable
{
   public event TransactionCompletedEventHandler TransactionCompleted;
   //Rest of the members
}

TransactionCompleted は、トランザクションが完了した後 (コミットまたは中止) 後に発生します。 イベントはデリゲート型 TransactionCompletedEventHandler で、2 つのパラメーターを受け取ります。sender は完了したトランザクション、e は TransactionEventArgs 型で、同じトランザクションへのアクセスも提供します。

例 13 に示すように、トランザクションの完了時に通知を受け取る他の関係者は、 TransactionCompleted イベントをサブスクライブできます。

例 13. トランザクション完了イベントの使用

public void DoTransactionalWork()
{
   using(TransactionScope scope = new TransactionScope())
   {
      Transaction transaction = Transaction.Current;
      transaction.TransactionCompleted += OnCompleted;
      /* Perform transactional work here */
      scope.Complete();
   }
}
void OnCompleted(object sender, TransactionEventArgs e)
{
   Debug.Assert(sender.GetType() == typeof(Transaction));
   Debug.Assert(Object.ReferenceEquals(sender,e.Transaction));
   Transaction transaction = e.Transaction;
   switch(transaction.TransactionInformation.Status)
   {
      case TransactionStatus.Aborted:
      {
         Trace.WriteLine("Transaction Aborted!");
      break;
      }
      case TransactionStatus.Committed:
      {
         Trace.WriteLine("Transaction Committed!");
      break;
      }
   }
}

開発者は LTM トランザクションがいつ開始するかを知りますが (スコープが構築されるとき)、コードでは、その LTM トランザクションが分散トランザクションに昇格されるタイミングを知る必要がある場合もあります。 静的クラス TransactionManager は、次のように定義 されたイベント TransactionStarted を 提供します。

public delegate void TransactionStartedEventHandler(object sender,
TransactionEventArgs e);

public static class TransactionManager
{
   public event TransactionStartedEventHandler 8
DistributedTransactionStarted;
   //Rest of the members
}

DistributedTransactionStarted イベントは、分散トランザクションが開始されるたびに発生します。 例 14 に示すように、分散トランザクションの開始イベントと完了イベントの両方をサブスクライブできます。

例 14. トランザクション開始イベントのサブスクライブ

public void DoTransactionalWork()
{
   TransactionManager.DistributedTransactionStarted += 
OnDistributedStarted;

   using(TransactionScope scope = new TransactionScope())
   {
      Transaction transaction = Transaction.Current;
      transaction.TransactionCompleted += OnCompleted;
      /* Perform transactional work here */
      scope.Complete();
   }
}
void OnDistributedStarted(object sender,TransactionEventArgs e)
{...}
void OnCompleted(object sender,TransactionEventArgs e)
{...}

分散トランザクションの開始イベント ハンドラーで行う作業に注意してください。 分散トランザクションは、すべてのイベント サブスクライバーに通知されるまで開始されないため、イベント処理メソッドは短い期間にする必要があります。

Code-Access セキュリティ

LTM トランザクションを使用するアプリケーションでは、SQL Server 2005 など、最大で 1 つの永続的なリソースからリソースを消費できます。 ただし、複数のリソースとやり取りできる分散トランザクションは、ネットワーク経由で行われる可能性があります。 これにより、悪意のあるコードによるサービス拒否攻撃、またはそのようなリソースの偶発的な過剰使用の両方の方法が開きます。 この問題を回避するために、 System.TransactionsDistributedTransaction セキュリティ アクセス許可を定義します。 トランザクションが LTM から分散トランザクションに昇格されるたびに、昇格をトリガーしたコードに DistributedTransaction アクセス許可が付与されていることが確認されます。 セキュリティアクセス許可の検証は、他のコード アクセス セキュリティ検証と同様に行われ、スタック ウォークを使用して、すべての呼び出し元に DistributedTransaction アクセス許可を要求します。 セキュリティの需要が昇格をトリガーしたコードに影響を与える点に注意してください。LTM トランザクションを最初に作成したコードとは限りません (ただし、同じ呼び出し履歴にある場合は確実にそうです)。

このアクセス許可の要求は、複数のリソースに対してトランザクション処理を実行する LocalInternet ゾーンなどの部分信頼環境にデプロイされたスマート クライアント アプリケーションにとって特に重要です。 定義済みの部分信頼ゾーンのいずれも 、DistributedTransaction アクセス許可を付与しません。 カスタム コード グループを使用してそのアクセス許可を付与するか、アプリケーションの ClickOnce 配置マニフェストでそのアクセス許可を手動で一覧表示する必要があります。 もう 1 つの解決策は、クライアント アプリケーションとリソースの間に中間層を導入し、中間層にこれらのリソースへのアクセスを移行的にカプセル化することです。

コンカレンシー管理と複製

クライアントに対して同時に作業を実行するワーカー スレッドを作成するトランザクション クライアントを想像してみてください。 クライアントは、ワーカー スレッドの作業を独自のトランザクションに作成したいと考えます。つまり、ワーカー スレッドで実行された作業が一貫している場合にのみ、クライアントのトランザクションがコミットされます。 ただし、このシナリオには 2 つの問題があります。 1 つ目は、トランザクションとマルチスレッドを混在させるという従来の問題です。トランザクションで同時作業が許可されている場合は、あるスレッドがトランザクションのコミットを試みる一方で、別のスレッドがトランザクションの中止を試みる場合があります。 次に、クライアントのアンビエント トランザクションは TLS に格納され、その結果、ワーカー スレッドに伝達されません。 System.Transactions には、トランザクション同時実行作業に関連する問題に対処するためのサポートが組み込まれています。

Transaction クラスは、次のように定義された DependentClone()メソッドを提供します。

Public enum DependentCloneOption
{
   BlockCommitUntilComplete,
   RollbackIfNotComplete,
}
[Serializable]
public class Transaction : ITransaction,ISerializable
{
   public DependentTransaction 8 DependentClone(DependentCloneOption cloneOption);
   //Rest of the members
}

DependentClone() は 、次のように定義された sealed クラス DependentTransaction のインスタンスを 返します。

[Serializable]
public sealed class DependentTransaction : Transaction
{
   public void Complete();
}

DependentTransactionTransaction から派生し、その唯一の目的は、複製されたトランザクションで実行された作業が完了し、コミットされる準備ができていることを (トランザクションの作成者に) 示すということです。 例 15 は、依存トランザクションを複製し、それをワーカー スレッドに渡す方法を示しています。

例 15. 別のスレッドによる依存クローンの使用

public class WorkerThread
{
   public void DoWork(DependentTransaction dependentTransaction)
   {
      Thread thread = new Thread(ThreadMethod);
      thread.Start(dependentTransaction);
   }
   public void ThreadMethod(object transaction)
   {
      DependentTransaction dependentTransaction;
      dependentTransaction = transaction as 8 DependentTransaction;
      Debug.Assert(dependentTransaction != null);
      Transaction oldTransaction = Transaction.Current;
      try
      {
         Transaction.Current = dependentTransaction;
      /* Perform transactional work here */
      dependentTransaction.Complete();
      }
      finally
      {
         dependentTransaction.Dispose();
      Transaction.Current = oldTransaction;
      }
   }
}
//Client code
using(TransactionScope scope = new TransactionScope())
{
   Transaction currentTransaction = Transaction.Current;
   DependentTransaction dependentTransaction;
   dependentTransaction = currentTransaction.DependentClone(8 DependentCloneOption.BlockCommitUntilComplete);
   WorkerThread workerThread = new WorkerThread();
   workerThread.DoWork(dependentTransaction);
   /* Do some transactional work here, then: */
   scope.Complete();
}

クライアント コードは、アンビエント トランザクションも設定するトランザクション スコープを作成します。 アンビエント トランザクションはコミット可能なトランザクションですが、そのトランザクションをワーカー スレッドに渡す必要はありません。 代わりに、クライアントは、現在のトランザクションで DependentClone() を呼び出すことによって、現在の (アンビエント) トランザクションを複製します。 DependentClone() は依存トランザクション オブジェクトを作成します。基になる返されるオブジェクトは、CommittableTransaction ではなく DependentTransaction です。 Transaction クラスには生の Clone() メソッドも用意されており、 CommittableTransaction (該当する場合) を含む、トランザクションの真の複製を返します。 このセクションの先頭に記載されている理由から、その危険な複製をワーカー スレッドに渡さないようにします。

WorkerThread クラスは、新しいスレッドで実行される ThreadMethod() を提供します。 クライアントは、依存トランザクションをスレッド メソッド パラメーターとして渡して、新しいスレッドを開始します。 ここでの問題は、完了時の同期です。ワーカー スレッドが完了する前にクライアントがトランザクション スコープの末尾に達した場合はどうなりますか? その場合、クライアントはどのようにしてトランザクションのコミットを試みることができるでしょうか。

これに対処するために、トランザクション オブジェクトは、作成したすべての依存クローンを追跡します。 DependentClone() メソッドは、DependentCloneOption 型の列挙型の cloneOption というパラメーターを受け取ります。cloneOptionDependentCloneOption.BlockCommitUntilComplete と等しい場合、クライアントがトランザクションをコミットしようとすると、すべての依存トランザクションが完了するまでクライアントはブロックされます。 例 15 では、 using ステートメントの 最後にトランザクション オブジェクトを破棄しようとして、クライアントがブロックされます。 cloneOptionDependentCloneOption.RollbackIfNotComplete の場合、クライアントはコミットを試みてブロックされません。 代わりに、アクティブな依存トランザクションがまだ存在し、クライアントがコミットを試みると、トランザクションが中止され、 TransactionAborted 例外がスローされます。 それだけでなく、ワーカー スレッドは影響を受けず、無駄にトランザクションで動作し続けます。 カスタムの手動同期が必要な場合を除き、 cloneOption は 常に DependentCloneOption.BlockCommitUntilComplete に設定します。

cloneOption をDependentCloneOption.BlockCommitUntilComplete に設定した場合でも、注意する必要がある追加のコンカレンシーの問題がいくつかあります。

  • ワーカー スレッドがトランザクションをロールバックしても、クライアントがトランザクションをコミットしようとすると、 TransactionAborted 例外がスローされます。
  • ワーカー スレッドは Complete() を 1 回だけ呼び出すことができます。それ以外の場合は InvalidOperation 例外が発生します。
  • トランザクション内のワーカー スレッドごとに、新しい依存クローンを作成してください。 同じ依存クローンを複数のスレッドに渡すことはありません。そのスレッドで Complete() を呼び出すことができるのはそのうちの 1 つだけであるためです。
  • ワーカー スレッドが新しいワーカー スレッドを生成する場合は、依存する複製から依存クローンを作成し、新しいスレッドに渡してください。

相互運用性

System.Transactions は ネイティブの .NET Enterprise Services をサポートします。サービス コンポーネントは、コードを変更せず、新しいトランザクション マネージャーとトランザクションの昇格を利用できます (例 2 を参照)。

興味深い質問は、トランザクション サービス コンポーネントが TransactionScope オブジェクトを作成するとき、またはトランザクション スコープがサービス コンポーネントを作成するときに何が起こるかです。 TransactionScope オブジェクトが Enterprise Services トランザクション コンテキストで実行される場合、Enterprise Services トランザクションをアンビエント トランザクションとして使用する必要がありますか? TransactionScope オブジェクトはどのエンタープライズ サービス コンテキストを使用する必要がありますか? このような混合と一致のシナリオで発生する問題は何ですか? Enterprise Services トランザクション プログラミング モデルは、オブジェクトのライフサイクルと状態管理に結合され、オブジェクトベースではないトランザクション スコープと組み合わせると、いくつかの複雑な副作用が発生する可能性があります。

System.Transactions は、 それ自体と Enterprise Services の間の 3 つのレベルの相互運用性を定義します。None、Automatic、Full です。 列挙型 EnterpriseServicesInteropOption は次のように定義されています。

public enum EnterpriseServicesInteropOption
{
   Automatic,
   Full,
   None
}

では、相互運用性レベルを指定できます。 TransactionScope クラスは、EnterpriseServicesInteropOption を受け入れるコンストラクターを提供します。次に例を示します。

public TransactionScope(
   TransactionScopeOption scopeOption,
   TransactionOptions transactionOptions,
   EnterpriseServicesInteropOption interopOption
);

EnterpriseServicesInteropOption.None は、名前が示すように、Enterprise Services コンテキストとトランザクション スコープの間に相互運用性がないことを意味します。 このようなトランザクション スコープは、トランザクション コンテキストまたはその作成クライアントを完全に無視し、独自のアンビエント トランザクションを使用します。 アンビエント トランザクションは、Enterprise Services マネージド トランザクションとは異なります。 その結果、例 16 に示すように、トランザクション スコープを囲む Enterprise Services トランザクションのコミット中にトランザクション スコープのトランザクションが中止される可能性があります。

例 16. ComplusInteropOption.None の使用

[Transaction]
public class MyService : ServicedComponent
{
   [AutoComplete]
   public void DoSomething()
   {
      TransactionOptions options = new TransactionOptions();
      options.IsolationLevel = IsolationLevel.Serializable;
      options.Timeout = TransactionManager.DefaultTimeout;
      using(TransactionScope scope = new TransactionScope(8
         TransactionScopeOption.Required, 
         options, 8
         EnterpriseServicesInteropOption.None) 
         )
      {
         //No call to scope.Complete(), yet the COM+
      //transaction still can commit
      }
   }
}

EnterpriseServicesInteropOption.None では、Enterprise Services トランザクションとアンビエント トランザクションの混在による副作用が排除されるため、最も安全なオプションです。 EnterpriseServicesInteropOption.None は、EnterpriseServicesInteropOption 値を受け入れられないすべてのコンストラクターで TransactionScope によって使用される既定値です。

EnterpriseServices トランザクションをアンビエント トランザクションと組み合わせる場合は、EnterpriseServicesInteropOption.Automatic または EnterpriseServicesInteropOption.Full を使用する必要があります。 どちらのオプションも ServiceDomain 関数に依存するため、Windows XP Service Pack 2 または Windows 2003 Server で実行する必要があります。 EnterpriseServicesInteropOption.Full は、サービス コンポーネントと同様に可能な限り動作しようとします。 TransactionScope オブジェクトにトランザクションが必要な場合 (既存のアンビエント トランザクションを結合するか、新しいアンビエント トランザクションを作成します)、EnterpriseServicesInteropOption.Full は新しい Enterprise Services トランザクション コンテキストを作成し、既存の Enterprise Services トランザクションにフローします (または新しい Enterprise Services トランザクションを作成します)。使用されるアンビエント トランザクションは、その Enterprise Services コンテキストで使用されるのと同じトランザクションになります。 TransactionScope オブジェクトにトランザクションが必要ない場合、スコープは既定の Enterprise Services コンテキストに配置されます。

たとえば、TransactionScope を作成し、トランザクション リソース マネージャーと対話するアプリケーションを考えてみましょう。 この RM のクライアント ライブラリが COM+ または System.EnterpriseServices トランザクションをサポートしているが、System.Transactions によって提供される Transaction.Current オブジェクトを尋問するように更新されていないとします。 この場合、 EnterpriseServicesInteropOption.Full を使用してスコープを作成すると、リソースがトランザクションに参加できるようになります。

EnterpriseServicesInteropOption.Automatic では、 EnterpriseServicesInteropOption.NoneEnterpriseServicesInteropOption.Full が組み合わされます。 EnterpriseServicesInteropOption.Automatic は、スコープが既定の Enterprise Services コンテキストで構築されているかどうかを確認します。

スコープが既定の Enterprise Services コンテキストで構築されている場合、Enterprise Services トランザクションはありません。 EnterpriseServicesInteropOption.AutomaticEnterpriseServicesInteropOption.None と同様に動作し、新しい Enterprise Services コンテキストは作成されません。 スコープが既定のコンテキスト以外のエンタープライズ サービス コンテキストで構築されている場合、 EnterpriseServicesInteropOption.AutomaticEnterpriseServicesInteropOption.Full のように動作します。

次の表 2 と 3 は、Enterprise Services コンテキストと、トランザクションを必要とする TransactionScope オブジェクトによって使用されるアンビエント トランザクションを示しています。

表 2 Enterprise Services コンテキストと、トランザクションを必要とするトランザクション スコープ。

ES コンテキスト None 自動 完全
既定のコンテキスト 既定のコンテキスト 既定のコンテキスト 新しいトランザクション コンテキストの作成
既定以外のコンテキスト クライアントのコンテキストの保守 新しいトランザクション コンテキストの作成 新しいトランザクション コンテキストの作成

表 3 アンビエント トランザクション トランザクショントランザクションを必要とするトランザクション スコープ。

ES コンテキスト None 自動 完全
既定のコンテキスト ST ST ES
既定以外のコンテキスト ST ES ES

ST — スコープのアンビエント トランザクションは、 によって System.Transactions管理され、存在する可能性のある Enterprise Services コンテキストを持つトランザクションとは別に行われます。

ES - スコープのアンビエント トランザクションは、Enterprise Services コンテキストのトランザクションと同じです。

リソース マネージャーの実装

System.Transactions は、 トランザクション リソース マネージャー (トランザクションのスコープ内でアクションが調整される成果物) を構築する開発者に新しいサポートを提供します。

リソース マネージャーは、通常、ライブラリを介してアプリケーションに公開されます。 そのライブラリ内でトランザクションを行うには、RM コードが Current トランザクションを調べ、アンビエント トランザクションに参加する必要があります (存在する場合)。 RM は永続的または揮発性の参加者として参加する場合があります。 Durable は、RM が永続状態を管理し、障害復旧をサポートすることを意味します。たとえば、1 つ以上のトランザクションが疑わしいか、まだ解決されていない間の障害後の回復などです。 揮発性の VM はメモリ内データ構造などの揮発性リソースを管理し、アプリケーションの再起動後に復旧を実行する必要はありません。

RM は、必要に応じて、トランザクション オブジェクトで EnlistVolatile または EnlistDurable を呼び出す必要があります。 参加している RM は、IEnlistmentNotification インターフェイスもサポートする必要があります。

public interface IEnlistmentNotification
{
   void Commit(Enlistment enlistment);
   void Prepare(PreparingEnlistment enlistment);
   void Rollback(Enlistment enlistment);
   void InDoubt(Enlistment enlistment);
}

このインターフェイスを使用して、トランザクション マネージャーはリソース マネージャーにトランザクション ライフサイクル イベントを通知します。 トランザクションが準備フェーズに達すると、トランザクション マネージャーは RM の Prepare() メソッドを呼び出します。 この時点で、永続リソース マネージャーは、このフェーズ中に準備レコードをログに記録する必要があります。 レコードには、参加リストの RecoveryInformation プロパティなど、回復を実行するために必要なすべての情報が含まれている必要があります。 この RecoveryInformation は、後続の復旧中に Reenlist() メソッドでトランザクション マネージャーに渡す必要があります。 その後、RM は、参加リストで Prepared() または ForceRollback() を呼び出して、トランザクションに投票します。

トランザクションがコミット ステージに到達すると、TM は Commit() メソッドを呼び出します。 RM はこのコールバックを使用して、ロックとログ レコードを解放できます。 完了すると、RM は参加リストで Done() を呼び出して、コミットの受信を確認する必要があります。

InDoubt() メソッドは、トランザクション マネージャーが単一の永続リソースに対して単一フェーズコミット操作を呼び出した後、トランザクション結果を取得する前に永続リソースへの接続が失われた場合に、揮発性 RM で呼び出されます。 その時点で、トランザクションの結果を安全に決定することはできません。 このメソッドの実装では、必要な回復操作または包含操作を実行し、作業が完了したら、enlistment パラメーターの の Done メソッドを呼び出す必要があります。

最後に、トランザクションをロールバックする必要がある場合、トランザクション マネージャーは Rollback() を呼び出します。 VM は作業を元に戻し、ロックを解除してから、参加オブジェクトに対して Done を呼び出す必要があります。

RM が前述のようにトランザクションの昇格をサポートしている場合は、EnlistPromotableSinglePhase メソッドを呼び出して参加させる必要があります。 この場合、RM は、前述のように、トランザクション マネージャーからシグナルを受信するために IPromotableSinglePhaseNotification インターフェイスを実装する必要があります。

まとめ

System.Transactions は、長い期限切れである革新的で実用的なアーキテクチャです。 アプリケーション プログラミング モデルをトランザクション管理から分離し、最適化されたパフォーマンスのためにトランザクション マネージャーの自動昇格を可能にし、トランザクション リソース マネージャーの構築を可能にし、将来のトランザクション マネージャーに対応する拡張可能なアーキテクチャです。 Windows Communication Foundation (コード名 "Indigo") や Windows Vista (コード名 "Longhorn") などのプラットフォームなどのテクノロジは、一貫性のあるトランザクション管理の基盤として System.Transactions に依存し、プラグ可能なプロバイダー モデルで新しいトランザクション マネージャーを提供します。 .NET Framework v2.0 の System.Transactions の最初のリリースは、堅牢で高性能なエンタープライズ アプリケーションを構築する上で開発に重要な追加機能です。

 

筆者について

Juval Lowy は、ソフトウェア アーキテクトであり、.NET アーキテクチャ コンサルティングと高度な .NET トレーニングを専門とする IDesign のプリンシパルです。 Juval は、Microsoft のシリコン バレー 地域ディレクター であり、業界での .NET の導入を支援する Microsoft と協力しています。 最新の書籍は、O'Reilly によって発行された .NET コンポーネントのプログラミングです。 Juval は、将来のバージョンの .NET の Microsoft 内部設計レビューに参加しています。 Juval は.NET 開発のほぼすべての側面に関する多数の 記事 を発表し、開発会議で頻繁に発表 しています。 Microsoft は、Juval を ソフトウェアの伝説として認識しました。これは、世界のトップ .NET エキスパートおよび業界リーダーの 1 つです。