印刷用ページ       送信     
クリックして評価とフィードバックをお寄せください
MSDN
MSDN ライブラリ
テクニカルドキュメント
.NET 開発
.NET Framework
.NET Framework 2.0
 .NET Framework 2.0 の System.Transac...

  低帯域幅での表示をオンにする
.NET Framework 2.0 の System.Transactions について

Juval Lowy
IDesign Inc.

May 2005
日本語版最終更新日 2005 年 10 月 17 日

概要: Microsoft .NET Framework 2.0 の System.Transactions 名前空間は、新しいトランザクション プログラミング モデルを導入しています。この記事では、この新しいトランザクション プログラミング モデルの機能や性能、さらに非同期処理やイベント、セキュリティ、同時実行管理やインターオペラビリティ (相互運用性) といった高度な機能を紹介します。

目次

はじめに
.NET 1.x トランザクション プログラミング モデル
.NET Framework Version 2.0 のトランザクション マネージャ
    トランザクション プロモーション
    プロモーションの誘発
System.Transactions との動作
    宣言型プログラミング モデル
    明示的プログラミング モデル
    トランザクション フローの管理
    TransactionScopeOption についての補足事項
    ネストされたスコープ内でのボーティング
    TransactionScope のタイムアウトの設定
    TransactionScope の分離レベルの設定
    TransactionScope のメリット
上級トピック
    トランザクション イベント
    コードアクセス セキュリティ
    同時実行管理とクローンの作成
    インターオペラビリティ (相互運用性)
まとめ
著者について

はじめに

従来、Microsoft Windows プラットフォームの開発者には、トランザクション プログラミング モデルとして、明示的トランザクション管理と宣言型トランザクション フローおよび管理という 2 つの選択肢が用意されています。どちらのプログラミング モデルがより優れているということはなく、どちらにも一長一短があります。.NET Framework の Version 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(); //データベースを登録します。
command.Transaction = transaction;
try
{
     /* ここでデータベースと情報交換を行い、トランザクションをコミットします。 */
     transaction.Commit();
}
catch
{
     transaction.Rollback(); //トランザクションをアボートします。
}
finally
{
     connection.Close();
}
                

接続オブジェクト上で BeginTransaction() を呼び出すことで、ベースになるデータベースを表すオブジェクトを取得します。BeginTransaction() は、トランザクション管理に使用される IDbTransaction インターフェイスの実装を返します。データベースに対して行われた更新や変更等がすべて一貫している場合は、トランザクション オブジェクト上で Commit() を呼び出します。何らかのエラーが発生している場合は、Rollback() を呼び出し、トランザクションをアボートします。

明示的プログラミング モデルは単純でありながら、トランザクションの実行クラスを必要としないため、図 1 に示すような、単一のオブジェクトが単一のデータベース (あるいは、単一のトランザクション リソース) とやりとりするような場合に適しています。

ms973865.introsystemtransact_01(ja-jp,MSDN.10).gif

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


理由は、トランザクション コーディネーションです。たとえば、図 2 で示すような、複数のオブジェクトが互いにやり取りし、オブジェクトは単一のリソース (データベース等の) とやり取りしているアプリケーションを考えてみましょう。この場合、どのオブジェクトがトランザクションを開始してリソースを登録するのでしょうか。もしすべてのオブジェクトでこれを行ってしまえば、複数のトランザクションができてしまいます。そうすると、トランザクションのコミットやロールバックはどのオブジェクトが行えばよいのでしょうか。自分以外のオブジェクトがトランザクションとどのような関係にあるか、どうすれば知ることができるでしょうか。トランザクション管理を行うオブジェクトが、残りのオブジェクトに対してトランザクションの結果を知らせるにはどうしたらよいでしょうか。もしかすると、オブジェクトは複数のプロセス間、あるいは複数のマシンにまたがって配置されることも考えられます。その場合は、ネットワークやマシンのクラッシュ等の問題により、トランザクションの管理はさらに複雑性を増します。解決策の 1 つとして、トランザクション コーディネーションのロジックを追加することにより、複数のオブジェクトを 1 つにまとめるといった方法が考えられますが、この方法は非常にもろく、ビジネス フローへのちょっとした変更や、トランザクションに参加するオブジェクト数のちょっとした変化にも持ちこたえることはできないでしょう。また、図 2 のオブジェクトの場合、複数ベンダーに分散して配置されていることも考えられます。その場合は、こうしたコーディネーションは不可能です。

ms973865.introsystemtransact_02(ja-jp,MSDN.10).gif

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


リソースが複数になると状況は著しく複雑になります (図 3 参照)。

ms973865.introsystemtransact_03(ja-jp,MSDN.10).gif

図 3. 複数オブジェクトによる複数リソースへのアクセス


1 つのトランザクションに複数のオブジェクトが関わっているという難題に加え、リソースが複数になることでさらに多くの障害発生点が生じることになり、トランザクションのコミットに部分的に失敗するといったことがリソース単位で起こりえます。上述の状況は分散トランザクションと呼ばれています。分散トランザクションは、2 つ以上の独立したグループまたはオブジェクト(異なる実行コンテキストにまたがることもあります)、あるいは 2 つ以上のトランザクショナル リソース、複数オブジェクトと複数リソースが関与するトランザクションです。分散トランザクションでは、起こりえるエラー ケースすべてを明示的に管理しようとすることは事実上不可能です。そのため、分散トランザクションの場合は、二層コミット プロトコルと専用トランザクション マネージャへの依存が必要です。

Windows の場合、分散トランザクション コーディネータ (DTC) システム サービスが、OLE Transactions (OleTx) と呼ばれるプロトコルを使用して、コンポーネントやプロセス、マシンにまたがるトランザクションを管理します。DTC に対して直接プログラミングすることも可能ですが、.NET の場合、DTC トランザクションを利用する最も一般的かつ簡単な方法は、System.EnterpriseServices 名前空間で利用可能な Enterprise Services の使用です。サンプル コード 2 は、Enterprise Services トランザクションの用法を示しています。

サンプル コード 2. Enterprise Services による宣言型トランザクション管理


using System.EnterpriseServices;

[Transaction]
public class MyComponent : ServicedComponent
{
     [AutoComplete]
     public void MyMethod()
     {
          /* その他のサービス コンポーネントやリソース マネージャと情報のやり取りをします。 */
     }
}
                

.NET Enterprise Services では宣言型プログラミング モデルを提供しています。つまり、ServicedComponent 抽象クラスから派生したクラスであれば、Transaction 属性を使用することができます。この属性は、当該クラスでメソッドが呼び出された際に、そのメソッドがトランザクション コンテキスト内で実行されることを保証します。コンテキスト とは、サービス コンポーネントの最も内部的な実行スコープです。.NET は、このコンテキストに入ってくる呼び出しをインターセプトして、オブジェクトの代わりにトランザクションを開始します。リソースの明示的な登録は必要ありません。言うまでもありませんが、この仕組みはトランザクションに自動的に登録できるリソースの場合にのみ機能します。このようなリソースは トランザクショナル リソース マネージャと呼ばれ、世間一般で使われている商用データベースや永続性の高いリソース (たとえば、Microsoft Message Queue 等) の多くは、リソース マネージャです。オブジェクトは、ただ、トランザクションをコミットするかアボートするかを .NET に対して伝えればよく、それ以外に何もする必要はありません。この処理は、ContextUtil ヘルパ クラスのメソッドを使っても実装できますが、AutoComplete メソッド属性でも宣言的に実現することができます。AutoComplete 属性は、メソッドに例外が発生しなければトランザクションをコミットし、例外が発生した場合は、トランザクションはアボートされます。

宣言型モデルは、生産性に著しいメリットがある一方で、欠陥がないわけではありません。

  • ServicedComponent からの継承を強制的に行うと、通常は内部的なアプリケーション モデリングのために予約されている基本クラスの重要部分が占有されてしまいます。
  • たとえ、単一のリソースおよび単一のオブジェクトしか使われていなくても、Enterprise Services トランザクションを使用するということは、分散 DTC トランザクションの使用を意味します。二層コミット プロトコルでは、リソースがその処理をログに記録する必要があるため、トランザクション マネージャ、リソースのいずれのレベルにおいても負担が大きくなります。
  • Enterprise Services を使用するということは、多くの開発者がプレッシャーに感じている、COM+ ホスティング モデルをベースにするということを意味します。
  • Enterprise Services トランザクションは、Enterprise Services のインスタンス管理ストラテジと密接に結びついています。すべてのトランザクション オブジェクトは JIT アクティベーションでアクティベートされます。そのため、トランザクションをオブジェクト プーリングと組み合わせるということになると、問題が生じます。この組み合わせは、スケーラブルなアプリケーションでは好んで使われる組み合わせですが、それ以外のアプリケーションの場合、ほとんどの開発者が扱いに苦労すると言われているステート認識のプログラミング モデルを余儀なくされます。
  • Enterprise Services トランザクションは常にスレッドセーフです。複数のスレッドが同一のトランザクションに参加する方法はありません。これにより、特にマルチスレッド環境におけるトランザクション管理が大きく簡素化されますが、一部のエッジ ケース (訳注: 発生する可能性はゼロではないけれども、あまり考えられないケース) においては制限となります。

事実、.NET 1.0 と 1.1 は、非分散トランザクションの使用と明示的トランザクション管理を同一視しており、また、分散トランザクションの使用と Enterprise Services による宣言型トランザクションも同一視しています。DTC トランザクションを使用せずに宣言型トランザクションを使用する方法はなく、DTC を利用する明示的トランザクション管理を実現する簡単な方法もマネージド コードには存在しません。プログラミング モデル (明示的か宣言型か) を選択するということは、トランザクション マネージャを選択しているということです (あるいはその逆)。

.NET Framework Version 2.0 のトランザクション マネージャ

上で述べた、明示的、宣言型トランザクション プログラミング モデルの問題に対応するため、.NET Framework Version 2.0 では 2 つの新しいタイプのトランザクション マネージャおよび管理名前空間を導入しています。これらは、Lightweight Transaction Manager (LTM) と OleTx Transaction Manager です。

LTM は、最大 1 つの永続的リソースが関与する単一アプリケーション ドメイン内でのトランザクション管理に使われます。OleTx Transaction Manager は、アプリケーション ドメイン境界 (プロセスやマシン境界も含まれます) をまたいだトランザクション、あるいは同一アプリケーション ドメイン内であっても、1 つ以上の永続的リソースが関与するトランザクションを管理します。OleTx トランザクション マネージャは、マシン間での呼び出しの際、RPC を使用し、多少の違いはありますが DTC の動作に相当します (これについては、本ホワイト ペーパーで後述します)。LTM はイントラアプリケーション ドメイン呼び出ししか使わないため、OleTx トランザクション マネージャに比べて高いパフォーマンスを実現できます。開発者は、これらトランザクション マネージャと直接やり取りする必要はありません。代わりに、インターフェイス、トランザクション ファクトリを定義する共通インフラストラクチャ、共通動作およびヘルパ クラスを System.Transactions 名前空間で利用できます。

これら 2 つのトランザクション マネージャで管理されるリソースは、System.Transactions Resource Managers と呼ばれます。Enterprise Services と同様、System.Transactions Resource Manager は、LTM または OleTx トランザクション マネージャに管理されるトランザクションに自動的に登録できるリソースです。

特定のトランザクション マネージャではなく、共通トランザクション管理名前空間 (System.Transactions) に対してプログラミングを行う大きなメリットは、プロモーションです。トランザクション マネージャ プロモーション は、System.Transactions がサポートする革新的なテクニックです。プロモーションの考え方はいたってシンプルです。すなわち、開発者がすべきことは希望のプログラミング モデル (明示的または宣言型トランザクション管理) を決定するだけで、後は、System.Transactions の方で適切なトランザクション マネージャを割り当ててくれます。

たとえば、単一の永続的リソースとやり取りしている単一オブジェクトがあるとします。この場合は、LTM だけでも、最高のスループットとパフォーマンスが実現できます。もし、同一マシン上の別アプリケーション ドメイン内の別オブジェクトにトランザクションを提供する、あるいは別の永続的リソース マネージャを登録したとすると、トランザクションは自動的に、OleTx による管理にプロモートされます。いったん、プロモートされたトランザクションは、トランザクションの完了まで、昇格されたステートで管理が継続されます。

LTM、OleTx いずれのトランザクション管理マネージャも、System.Transactions 名前空間で定義されている、Transaction と呼ばれるクラスを使ってトランザクションを表します。Transaction は、トランザクションへのリソースの登録、トランザクションのアボート、分離レベルの設定、トランザクション ステータスおよび ID の取得、トランザクションのクローン作成等に使われます。トランザクションをコミットする場合、System.TransactionsCommittableTransaction クラスを次のように定義します。


[Serializable]
public class Transaction : IDisposable,ISerializable
{
     public void Rollback(); // トランザクションをアボートします。
     public static Transaction Current{get;set;}
     public void Dispose();
     // その他のメンバ
}
[Serializable]
public sealed class CommittableTransaction : Transaction,IAsyncResult
{
     public void Commit();
     // その他のメンバ
}
                

1 つではなく、クラスが 2 つ用意されている理由については、後で説明します。

System.Transactions を使用する場合、ベースになるリソース マネージャと直接対話することはできません。そうすると、プロモーション メカニズムを飛び越えてしまうからです。その代わりに、常に、暗黙、明示的に関係なく、Transaction またはそのサブ クラスを使用します。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 はりソース マネージャに対し、LTM トランザクションの制御を手放すよう伝え、代わりに LTM に OleTx トランザクションに参加することのできるトランザクション オブジェクトを提供するよう伝えなければいけません。この対話をサポートするために、リソース マネージャは、次のように定義した IPromotableSinglePhaseNotification インターフェイスを実装する必要があります。


public interface ITransactionPromoter
{
    byte[] Promote();
}
public interface IPromotableSinglePhaseNotification : ITransactionPromoter
{
     void Initialize();
     void Rollback(SinglePhaseEnlistment singlePhaseEnlistment);
     void SinglePhaseCommit(SinglePhaseEnlistment singlePhaseEnlistment);
}
                

LTM トランザクションがリソース マネージャにアクセスしたとき、リソース マネージャはそのアクセスを検出する必要があり、その上で、現在の LTM トランザクション オブジェクトへの参照を取得して登録を行います。この処理は、オートエンリストメントと呼ばれます。登録が完了すると、LTM はりソース マネージャに対し、IPromotableSinglePhaseNotification の実装を問い合わせます。LTM トランザクションのコミットまたはアボートの呼び出しは、IPromotableSinglePhaseNotification (それぞれ、SinglePhaseCommit()Rollback())での呼び出しに変換されます。

LTM のオーバーヘッドは極めて小さく、マイクロソフトが SQL Server 2005 を使用して行ったパフォーマンス ベンチマークテストでは、LTM トランザクションを使用した場合とネイティブ トランザクションを直接使用した場合とでは、両者における統計上の差はまったく見られませんでした。

System.Transactions を使用することで、リソース マネージャ ベンダーは、上述のやり取りを簡単にサポートできるようになります。トランザクションへの参照を取得する場合は、リソース マネージャから Transaction の静的プロパティの Current を呼び出します。トランザクションへの登録をするときは、永続的なリソースの場合は Transaction オブジェクトの EnlistDurable() メソッド、揮発的なリソース (ステートをメモリにしか保存しないリソース) の場合は EnlistVolatile() メソッドを、それぞれリソース マネージャから呼び出します。


[Serializable]
public class Transaction : IDisposable,ISerializable
{
     public Enlistment EnlistDurable(...);
     public Enlistment EnlistVolatile(...);
     // その他のメンバ
}
                

LTM が、トランザクションを OleTx トランザクションにプロモーションすると決定した場合、ITransactionPromoterPromote() を呼び出すだけです。このとき、内部的には、リソース マネージャが、トランザクションを (ログインを必要としない) ローカル トランザクションから分散トランザクションへの変換処理を行います。この処理には、そのリソースがあたかも最初から DTC または OleTx トランザクションに登録されていたかのように感じさせる効果があります。

IPromotableSinglePhaseNotification の実装は、プロモート可能トランザクションへの参加においてキーとなる部分であるため、SQL Server 2000 や MSMQ、Oracle Database、IBM DB2 等の既存の Enterprise Services リソース マネージャーは、LTM トランザクションに参加することができません。これらのリソースが LTM トランザクションにアクセスされた場合、関与するリソースが 1 つしかないとしても、トランザクションは自動的に OleTx トランザクションにプロモートされます。

プロモーションの誘発

プロモーションを誘発する事象 (イベント) としては次の 2 種類があります。1 つ目は、新たな永続的リソース マネージャのトランザクションへの登録です。たとえば、SQL Server 2005 データベースに対して 2 つ目の接続をオープンすると、データベースの登録が行われます。LTM は、このデータベースがトランザクション内の 2 つ目の永続的リソースであると判断すると、トランザクションを OleTx トランザクションにプロモーションします。

2 つ目のプロモーション トリガは、アプリケーション ドメイン境界を超えたトランザクション オブジェクトのシリアライゼーションです。Transaction は、値マーシャリングのオブジェクト、つまり、アプリケーション ドメイン境界を超えて (同一プロセス内であっても) オブジェクトの引渡しを行おうとすると、トランザクション オブジェクトのシリアライゼーションが行われるオブジェクトです。

トランザクション オブジェクトの引き渡しは、(Transaction を引数に取るリモート メソッド上で呼び出しを実行することで) 自分で行うことができます。あるいは、リモート トランザクショナル サービス コンポーネントにアクセスすることも可能です。この場合も、トランザクション オブジェクトのシリアライズが行われます。トランザクションをシリアライズするということは、結局、トランザクションのプロモートを意味します。なぜなら、アプリケーション ドメインをまたいでシリアライズを行った場合、実際にはトランザクションを分散することになり、LTM (パススルーとしての役割を果たしているにすぎませんが) では役不足になってしまいます。LTM トランザクション クラスはカスタム シリアライゼーションを使用します。シリアライゼーション リクエストの処理において、トランザクションのプロモートを行い、デシリアライゼーションの処理においては、新しい OleTx トランザクションの新規アプリケーション ドメインに登録を行います。

System.Transactions との動作

System.Transactions により、ついにトランザクション マネージャからアプリケーションプログラミング モデルを切り離すことができました。System.Transactions からオブジェクトにアクセスすることで、トランザクションに対して明示的にプログラムを行うアプリケーションを開発することができます。Enterprise Services の宣言型トランザクションを利用してもかまいません。

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

.NET 2.0 の宣言型トランザクショナル プログラミングは、.NET Enterprise Services に依存しています。マイクロソフトは、System.Transactions を有効活用できるよう、ADO.NET の書き換えを行っています。Enterprise Services の使用については、.NET 1.1 のときと何ら変わりはありません。サンプル コード 2 で提示されたものと同じ Enterprise Services コードで、可能な場合には自動的に LTM が使われ、必要であれば OleTx にプロモートします。これにより、リソースまたはローカル オブジェクトを単独で扱う場合のパフォーマンス ペナルティを解消しながら、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, 
TransactionOptions transactionOptions);
     public TransactionScope(TransactionScopeOption scopeOption, 
TimeSpan scopeTimeout);
     // 追加のコンストラクタ
}
                

名前が示すように、TransactionScope クラスはトランザクションに関するコード セクションの範囲設定に使われます (サンプル コード 3 参照)。内部的な処理としては、コンストラクタにおいて TransactionScope オブジェクトがトランザクションを生成し (.NET 2.0 のデフォルトでは LTM)、Transaction クラスの Current プロパティの設定により生成されたトランザクションをアンビエント トランザクションとしてアサインします。TransactionScope はディスポーザブルなオブジェクトです。つまり、トランザクションは、Dispose() メソッドが呼ばれた時点 (サンプル コード 3 の using ステートメントの最後) で終了します。

サンプル コード 3. TransactionScope クラスを使用する


using(TransactionScope scope = new TransactionScope())
{
     /* トランザクション処理を行います。 */
     // エラー無し - トランザクションをコミットします。
     scope.Complete();
}
                

また、Dispose() メソッドは、アンビエント トランザクションを元の状態 (サンプル コード 3 の場合は null) に戻します。

トランザクション スコープ内 (通常は、using ステートメント内) の コードの実行に時間がかかる場合は、トランザクション デッドロックにつながる可能性があります。この問題を解消するため、トランザクションは、あらかじめ設定されたタイムアウト値 (デフォルトでは 60 秒) を超えると、自動的にアボートします。このタイムアウト値はプログラム的に変更することもできますし、設定ファイルからも変更することができます。

最後にもう一点、using ステートメント内で使われなかった TransactionScope オブジェクトは、トランザクションがタイムアウトになりアボートされた時点で、ガベージとなることがあります。

TransactionScope オブジェクトには、トランザクションがコミットすべきか、アボートすべきかを判断する手段がありません。TransactionScope の主要目的は、トランザクションと直接対話する必要性を開発者から隠ぺいすることです。この対応策として、各 TransactionScope オブジェクトには、consistency というビットが用意されており、デフォルトでは false に設定されています。consistency ビットは、Complete() メソッドを呼び出すことで true に設定できます。ただし、Complete() への呼び出しは一度しかできないので注意が必要です。それ以上、Complete() 呼び出しを行うと、InvalidOperation 例外が発生します。これは、開発者が Complete() 呼び出しの後には、トランザクショナル コードを使わせないように仕向けるための、意図的な仕様です。

(Dispose() 呼び出し、またはガベージ コレクションにより) トランザクションが終了し、consistency ビットに false がセットされた場合、そのトランザクションはアボートされます。たとえば、次のような scope オブジェクトの場合は、consistency ビットが規定値から変更されることがないため、トランザクションは常にロールバックされます。


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

一方、Complete() を呼び出し、サンプル コード 3 のように、consistency ビットに true がセットされた状態でトランザクションが終了した場合、そのトランザクションはコミットされます。ただし、Complete() の実行後は、アンビエント トランザクションにアクセスすることがでません。無理にアクセスしようとすると、不正な処理例外が発生するので注意が必要です。アンビエント トランザクションには、(Complete() を実行した) スコープ オブジェクトが破棄された時点で、(Transaction.Current により) 再度アクセスできるようになります。スコープ内のコードが Complete() を実行したという事実だけでは、トランザクションのコミットは保証されません。そのため、トランザクションのコミットに失敗すると、Dispose()TransactionAbortedException を発生させます。この例外は、サンプル コード 4 で示すように、ユーザーに警告メッセージを出すことで、例外処理を行うことができます。

サンプル コード 4. TransactionScope とエラー処理


try
{
     using(TransactionScope scope = new TransactionScope())
     {
          /* トランザクション処理を行います。 */
          // エラー無し - トランザクションをコミットします。
          scope.Complete();
     }
}
catch(TransactionAbortedException e)
{
     MessageBox.Show(e.Message);
}
catch // その他の例外
{
     Trace.WriteLine("Cannot complete transaction");
     throw;
}
                

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

トランザクション スコープは、直接的、間接的、いずれの方法でもネストすることができます。直接スコープ ネスティング は、サンプル コード 5 に示すように、1 つのスコープの中にもう 1 つのスコープを入れ子にした状態をいいます。


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

サンプル コード 5. 直接スコープ ネスティング

間接スコープ ネスティングは、サンプル コード 6 の RootMethod() のように、独自のスコープを使用するメソッド内から TransactionScope を使用するメソッドを呼び出している状態をいいます。

サンプル コード 6. 間接スコープ ネスティング


void RootMethod()
{
     using(TransactionScope scope = new TransactionScope())
     {
          /* トランザクション処理を行います。 */
          SomeMethod();
          scope.Complete();
     }
}

void SomeMethod()
{
     using(TransactionScope scope = new TransactionScope())
     {
          /* トランザクション処理を行います。 */
          scope.Complete();
     }
}
                

スコープ ネスティングは、直接、間接ネスティングの両方を用い、複数使用することができます。一番外側のスコープを ルート スコープ といいます。当然ながら、ルート スコープと残りのネストされたスコープとの関係はどのようになっているのかという疑問が生じます。また、スコープのネストはアンビエント トランザクションにどのような影響を及ぼすのでしょうか。すべてのスコープは同一のトランザクションに参加するのでしょうか。さらに、ネストされたスコープ内で、トランザクションのボーティングを行った場合、中にあるスコープには影響ないのでしょうか。

こうした疑問を解消する目的で、TransactionScope クラスには、次のように定義された、列挙型の TransactionScopeOption を受け入れるオーバーロードされたコンストラクタがいくつか用意されています。


public enum TransactionScopeOption
{
     Required,
     RequiresNew,
     Suppress
}
                

TransactionScopeOption の値により、スコープをトランザクションに参加させるか否か、参加させるのであれば、アンビエント トランザクションに参加させるのか、新しいトランザクションのルート スコープにさせるのかの制御が可能です。

たとえば、以下は、スコープのコンストラクタに TransactionScopeOption の値を指定する用法を示したものです。


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

スコープ オプションのデフォルト値は TransactionScopeOption.Required で、この値は、TransactionScopeOption パラメータを受け入れないコンストラクタを使用した際に使われる値であるということを意味します。

TransactionScope オブジェクトは、オブジェクトの構築時に所属するトランザクションを決定します。いったん、所属するトランザクションが決まると、スコープは常にそのトランザクションに所属します。TransactionScope は、アンビエント トランザクションの存在の有無、TransactionScopeOption パラメータの値という 2 つの要因をもとに所属するトランザクションを決定します。

TransactionScope オブジェクトには次の 3 つの選択肢があります。

  • アンビエント トランザクションに参加する。
  • 新しいスコープ ルートになる。つまり、新しいトランザクションを開始し、そのトランザクションに、そのスコープ内の新しいアンビエント トランザクションになってもらう。
  • トランザクションには一切参加しない。

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

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

スコープが TransactionScopeOption.Suppress で設定されている場合は、アンビエント トランザクションの有無に関係なく、トランザクションには参加しません。TransactionScopeOption.Suppress で設定されたスコープの場合、そのアンビエント トランザクションは常に null となります。

TransactionScopeOption の割り振り決定表 (ディシジョン テーブル) を表 1 にまとめてあります。

表 1. TransactionScopeOption の決定表 (ディシジョン テーブル)

TransactionScopeOptionアンビエント トランザクションスコープの参加先トランザクション
Required なし(ルートとなる) 新規トランザクション
Requires Newなし(ルートとなる) 新規トランザクション
Suppressなしなし
Required ありアンビエント トランザクション
Requires Newあり(ルートとなる) 新規トランザクション
Suppressありなし

TransactionScope オブジェクトが既存のアンビエント トランザクションに参加した場合、スコープ オブジェクトの破棄だけではトランザクションは終了しません。アンビエント トランザクションがルート スコープに作成されていた場合、ルート スコープが破棄されてはじめてトランザクションの終了となります。アンビエント トランザクションが手動で作成されていた場合は、作成者にコミットされるかアボートされてはじめて、トランザクションの終了となります。

サンプル コード 7 は、それぞれ異なる TransactionScopeOption 値を持つ、3 つのスコープ オブジェクトを生成する TransactionScope オブジェクトを示しています。図 4 は、このコードの結果として作成されたトランザクションを図に表したものです。

サンプル コード 7. スコープをまたいだトランザクション フロー


using(TransactionScope scope1 = new TransactionScope())
// デフォルトは Required。

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

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

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

     ...
}
                

ms973865.introsystemtransact_04(ja-jp,MSDN.10).gif

図 4. トランザクション スコープをまたいだトランザクション フロー


サンプル コード 7 には、アンビエント トランザクションを作成せず、TransactionScopeOption.Required で新規 TransactionScope (スコープ1)を作成するコード ブロックが記載されています。スコープ1 はルート スコープになります。スコープ1 は新規トランザクション (トランザクション A)を作成し、このトランザクション A をアンビエント トランザクションとします。続いて、スコープ1 は、それぞれ異なる TransactionScopeOption 値を持つ、3 つのオブジェクトを生成しに行きます。たとえば、スコープ2 は、Required をサポートするよう設定されています。さらに、アンビエント トランザクションが存在しているため、スコープ2トランザクション A に参加します。このとき、スコープ3 は新規トランザクション、トランザクション B のルート スコープとなっている点、スコープ4 にはトランザクションが存在しない点に注目してください。

TransactionScopeOption についての補足事項

TransactionScopeOption.Required は、TransactionScopeOption のデフォルト値であり、最もよく使われる値ではありますが、この他の値も色々と便利です。

TransactionScopeOption.Suppress は、コード セクションで実行される処理があるとよい場合や、処理が失敗してもアンビエント トランザクションをアボートさせたくない場合に便利です。サンプル コード 8 に示すように、TransactionScopeOption.Suppress を使用することで、トランザクショナル スコープ内に非トランザクショナル コード セクションを持つことが可能になります。

サンプル コード 8. TransactionScopeOption.Suppress の使用


using(TransactionScope scope1 = new TransactionScope())
{
     try
     {
          // 非トランザクショナル セクションを開始します。
          using(TransactionScope scope2 = new 
      TransactionScope(TransactionScopeOption.Suppress))
          {
               // 非トランザクショナル処理を実行します。
          }
          // アンビエント トランザクションを復元します。
     }
     catch
     {}
     // スコープ1 の残り部分
}
                

上記のほかに、TransactionScopeOption.Suppress は、カスタム動作を取り入れたい場合や、プログラムベースの独自トランザクション サポートを実行する、あるいは手動でリソースを登録する必要がある場合などに役立ちます。

そうはいっても、トランザクショナル スコープと非トランザクショナル スコープを同時に使用する場合は、独立性や一貫性が損なわれる可能性があるため、注意が必要です。非トランザクショナル スコープでは、エラーが発生すると、トランザクション結果に反映させることができません (この場合、一貫性が損なわれる恐れがあります)。また、非トランザクショナル スコープは、まだコミットされていない情報をもとに動作することができてしまいます (この場合は、独立性が損なわれる恐れがあります)。

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

TransactionScopeOption.RequiresNew 値を使用する際は特に注意が必要です。2 つあるトランザクション (アンビエント トランザクションとスコープに対して作成されたトランザクション) のうち片方がアボートされ、もう片方がコミットされるということがあります。その場合は、一貫性が損なわれないよう細心の注意を払う必要があります。

ネストされたスコープ内でのボーティング

ネストされた (入れ子状態にある) スコープは、親スコープのアンビエント トランザクションに参加することができますが、それぞれのスコープ オブジェクトの consistency ビットは互いに独立しているということを認識しておいてください。ネストされたスコープ内で Complete() を実行しても、親スコープには何の影響もありません。


using(TransactionScope scope1 = new TransactionScope())
{
     using(TransactionScope scope2 = new TransactionScope())
     {
          scope2.Complete();
     }
     // スコープ1 の consistency ビットは false のままです。
}
                

ルート スコープから一番内側のネスト スコープまで、すべてのスコープがコミットに賛成して初めて、トランザクションはコミットされます。

TransactionScope のタイムアウトの設定

オーバーロードされた TransactionScope のコンストラクタの中には、TimeSpan という型の値を受け入れるものがあります。この値は、トランザクションのタイムアウトを制御するのに使用します。以下はその用例です。


public TransactionScope(TransactionScopeOption scopeOption,TimeSpan scopeTimeout);
                

60 秒のデフォルト値以外の値を指定したい場合は、指定したい値を渡すだけです。


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

ゼロに設定されたタイムアウトは無限タイムアウトを意味します。無限タイムアウトは、コードをステップ スルー実行することでビジネス ロジックから問題の切り分けを行いたいときや、問題の特定時、デバッグ対象のトランザクションをタイムアウトさせたくない場合など、主にデバッグ作業の際に便利です。ただし、無限タイムアウトの使用は、トランザクション デッドロックに対してセーフガードが存在しないということを意味します。そのため、いずれの場合においても、無限タイムアウトを扱うときは細心の注意を払うようにしてください。

TransactionScope タイムアウトを規定値以外の値に設定するケースとしては、通常、2 通りあります。1 つ目は開発中で、アボートされたトランザクションをアプリケーションがどのように処理するかをテストしたい場合です。TransactionScope タイムアウトに小さな値 (たとえば、1 ミリ秒等) を設定すると、トランザクションを失敗させることができるので、エラー処理コードの動作確認ができます。2 つ目のケースは、TransactionScope のトランザクション タイムアウトを規定のタイムアウトよりも小さい値に設定するケースで、どう考えてもスコープがリソースの競合に巻き込まれ、デッドロックが発生すると考えられる場合です。そのような場合は、一刻も早くトランザクションをアボートしたい、規定のタイムアウト時間を待ってはいられないと考えるでしょう。問題は、TransactionScope がアンビエント トランザクションに参加したときに、そのアンビエント トランザクションが設定しているタイムアウトとは異なる値を設定しているとどうなるかということです。ネストされたスコープのタイムアウト値がアンビエント トランザクションのタイムアウト値よりも小さいと、アンビエント トランザクションでは新しい、短い方のタイムアウトが実行されるという影響があります。その場合、トランザクションはネストされたスコープに指定された時間内に終了しなければいけません。そうしないと、トランザクションは自動的にアボートされます。ネストされたスコープのタイムアウト値が、アンビエント トランザクションのタイムアウト値よりも大きい場合は、特に影響はありません。

TransactionScope の分離レベルの設定

オーバーロードされた TransactionScope のコンストラクタの中には、次のように定義された TransactionOptions という型の構造体を受け入れるものがあります。


public struct TransactionOptions
{
     public IsolationLevel IsolationLevel{get;set;}
     public TimeSpan Timeout{get; set;}
     // その他のメンバ
}
                

TransactionOptions Timeout プロパティはタイムアウトの指定に使用することができますが、TransactionOptions の本来の用途は、分離レベルの指定です。トランザクションのデフォルトでは、分離レベルを Serializable に設定した状態で実行されます。しかしながら、TransactionOptions IsolationLevel プロパティに、次のように定義された列挙型の IsolationLevel を指定することも可能です。


public enum IsolationLevel
{
     ReadUncommitted,
     ReadCommitted,
     RepeatableRead,
     Serializable,
     Unspecified,
     Chaos,
     // 分離レベルなし
     Snapshot // SQL Server 2005 でサポートされている、Read Committed (コミット済み読み取り) という特殊形式
}
                

サンプル コード 9 は、分離レベルの指定方法を示しています。

サンプル コード 9. 分離レベルの指定


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

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

直列化 (Serializable) 以外の分離レベルの選定は、一般的に、読み取りが集中するシステムで必要となります。分離レベルの選定には、トランザクション処理のセオリー、トランザクションのセマンティクス、同時実行に関する問題、システムの一貫性への影響等を正しく理解している必要があります。さらに、すべてのリソース マネージャがすべての分離レベルをサポートしているわけではありません。また、実際に設定されている分離レベルよりも上のレベルが設定されたトランザクションに参加することになってしまうことも考えられます。Serialized だけでなく、どの分離レベルも、同じ情報にアクセスしている別のトランザクションが原因となる何らかの不整合の影響を受けやすくなっています。4 つの分離レベルの違いは、各分離レベルの読み取りロックおよび書き込みロックの使い方にあります。ロックは、トランザクションがリソース マネージャのデータにアクセスする場合のみ、あるいはトランザクションがコミットまたはアボートされるまで、保持することができます。スループットを考えた場合は、前者が適しており、一貫性を考えた場合は後者が適しています。この 2 種類のロックと 2 種類の操作 (読み取り/書き込み) をもとに、4 種類の分離レベルが定義されています。なお、分離レベルの詳細については、トランザクション処理に関するテキストブックを参照してください。

ネストされた TransactionScope オブジェクトを使用する場合、アンビエント トランザクションに参加させたければ、すべてのネスト スコープで、全く同じ分離レベルを使用するよう設定しなければいけません。ネストされた TransactionScope オブジェクトがアンビエント トランザクションに参加しようとしたときに、異なる分離レベルを設定していると、ArgumentException 例外が発生します。

TransactionScope のメリット

サンプル コード 1 のプログラミング モデルと比較した場合、TransactionScope オブジェクトには次のような利点があります。

  • トランザクショナル スコープ内のコードはトランザクショナルであるだけでなくプロモータブルである。トランザクションは 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
{
     /* トランザクション処理を行います。 */
     // エラー無し - トランザクションをコミットします。
     committableTransaction.Commit();
}
finally
{
     committableTransaction.Dispose();
     // アンビエント トランザクションを復元します。
     Transaction.Current = oldAmbient;
}
                

サンプル コード 10 は、サンプル コード 3 の、TransactionScopeOption.Required を設定した TransactionScope と同じ処理を行っています。サンプル コード 10 は、Transaction クラスの静的プロパティ Current によりアンビエント トランザクションへの参照を取得するところから始まります。Current の型は Transaction です。Transaction にはたくさんの必須メソッドがありますが、Commit() メソッドは用意されていません。

これは、トランザクションを選んでもらうためにトランザクション オブジェクト (あるいはそのクローン) を別のパーティー (場合によっては別スレッド上のパーティー) に引渡したとしても、トランザクションのコミット自体は自身で行うことができるようにするための、意図的な仕様です。前述したように、トランザクションをコミットするには、CommittableTransaction を使用する必要があります。サンプル コード 11 に、CommittableTransaction の一覧をリストします。

サンプル コード 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;
                

また、古いアンビエント トランザクションは、いったん保存し、CommittableTransaction オブジェクトを使った作業が終了したら、Finally ステートメントで復元するようにしてください。

サンプル コード 10 は、TransactionScope の実際の処理を分かりやすく説明したものですが、実は、CommittableTransaction の興味深い用法 - 非同期コミットが示されています。トランザクションをコミットした場合、複数データベースへのアクセス、ネットワークの遅延などの理由で、コミットに時間がかかってしまうことがあります。トランザクションの実行時間を気にしつつも (特に、デッドロックを解決する場合)、トランザクション処理を一刻も早く終了し、実際のコミット処理はバックグラウンドで実行してしまいたいと思うでしょう。このような場合には、CommittableTransactionBeginCommit() メソッドと EndCommit() メソッドが適しています。これらのメソッドは、サンプル コード 12 に示すように、.NET における非同期呼び出しと同様に使用します。BeginCommit() を呼び出すと、スレッド プールからのスレッドにコミット ホールドアップがディスパッチされます。

このとき、EndCommit() を呼び出して、トランザクションが本当にコミットしていることを確認する必要があります。トランザクションが失敗していた場合 (いかなる理由であれ)、EndCommit() はトランザクション例外を発生します。EndCommit() を呼び出した時点でトランザクションがまだコミットしていない場合は、トランザクションがコミット (またはアボート) されるまで、呼び出し側をブロックします。コミットを非同期で呼び出す最も簡単な方法は、コミット処理が終了した時点で呼び出される、コールバック メソッドを提供することです。EndCommit() は、この呼び出しを実行した大元のコミット可能トランザクション オブジェクト上で呼び出さなければいけません。ですが、CommittableTransactionIAsyncResult 派生のオブジェクト、つまり同じオブジェクトであるため、この大元のオブジェクトは、コールバック メソッドの IAsyncResult パラメータをダウンケーシングすることで簡単に取得することができます。ちなみに、コミットが完了するまでは、トランザクションはリソース マネージャ内にロックを保持します (分離レベルにもよります)。

サンプル コード 12. トランザクションを非同期にコミットする


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

     try
     {
          /* トランザクション処理を行います。 */
          // エラー無し - トランザクションを非同期にコミットします。
          committableTransaction.BeginCommit(OnCommitted,null);
     }
     finally
     {
          // アンビエント トランザクションを復元します。
          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)
     {
          // コミットの失敗を処理します。
     }
}
                

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

Transaction クラスには、次のように定義された、TransactionCompleted という名前のパブリック イベントが用意されています。


public delegate void TransactionCompletedEventHandler(object sender,TransactionEventArgs e);
public class TransactionEventArgs: EventArgs
{
     public TransactionEventArgs();
     public Transaction Transaction{get;}
}
[Serializable]
public class Transaction : IDisposable,ISerializable
{
     public event TransactionCompletedEventHandler TransactionCompleted;
     // その他のメンバ
}
                

TransactionCompleted イベントは、トランザクションが完了 (つまり、コミットされるかアボートされた状態) すると発生します。イベントは、delegate 型の TransactionCompletedEventHandler で、完了したばかりのトランザクションを表す sender と、TransactionEventArgs 型である e の 2 つのパラメータを取り、同じトランザクションへのアクセスも提供します。

サンプル コード 13 で示すように、TransactionCompleted イベントは、通知を必要としている別のパーティーに対してサブスクライブすることができます。

サンプル コード 13. トランザクション完了イベントの使用


public void DoTransactionalWork()
{
     using(TransactionScope scope = new TransactionScope())
     {
          Transaction transaction = Transaction.Current;
          transaction.TransactionCompleted += OnCompleted;
          /* トランザクション処理を行います。 */
          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 トランザクションがいつ分散 OleTx トランザクションにプロモートされたかを分かっておいた方がよいかもしれません。静的クラスの TransactionManager には、次のように定義された TransactionStarted イベントが用意されています。


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

public static class TransactionManager
{
     public event TransactionStartedEventHandler DistributedTransactionStarted;
     // その他のメンバ
}
                

DistributedTransactionStarted イベントは、分散トランザクションが開始されると必ず発生します。サンプル コード 14 で示すように、分散トランザクションの開始と完了、両方のイベントに対してサブスクライブすることができます。

サンプル コード 14. トランザクション開始イベントに対するサブスクライブ


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

     using(TransactionScope scope = new TransactionScope())
     {
          Transaction transaction = Transaction.Current;
          transaction.TransactionCompleted += OnCompleted;
          /*  トランザクション処理を行います。 */
          scope.Complete();
     }
}
void OnDistributedStarted(object sender,TransactionEventArgs e)
{...}
void OnCompleted(object sender,TransactionEventArgs e)
{...}
                

分散トランザクションの開始イベント ハンドラに記述する内容については注意が必要です。分散トランザクションは、すべてのイベント サブスクライバに通知が送信されるまで、トランザクションを開始しません。そのため、イベントを処理するメソッドは短期間で終了しなければいけません。

コードアクセス セキュリティ

LTM トランザクションを使用するアプリケーションの場合、せいぜい 1 つの永続的リソース (SQL Server 2005 等) のリソースしか使うことができませんが、複数リソースとの対話 (場合によってはネットワーク経由による) が可能な分散トランザクションの場合においてはこの限りではありません。ただし、その場合は、悪意のあるコードによる DoS 攻撃や、リソースの予想外の過剰消費を許してしまいます。こうした状況を防ぐために、System.Transactions は、DistributedTransaction というセキュリティ権限を定義しています。トランザクションが LTM から OleTx トランザクションにプロモートされるときは必ず、プロモーションをトリガしたコードに DistributedTransaction 権限があるかどうか、検証されるようになっています。セキュリティ権限の検証は、他のコードアクセス セキュリティ検証と同じで、スタック ウォークを実行し、スタック上の各呼び出し側に DistributedTransaction を要求します。何度も言うようですが、セキュリティ要求が影響するのは、あくまでもプロモーションをトリガしたコードであって、必ずしも LTM トランザクションを作成したコードではないということに注意してください (もちろん、これらが同一コール スタック上にあることが前提です)。

この権限要求は、LocalInternet ゾーンなどの部分的に信頼された環境に配置された、トランザクション処理を複数リソースに対して実行したい Smart Client アプリケーションにとっては非常に重要です。DistributedTransaction を付与する、あらかじめ定義された部分的に信頼されたゾーンはありません。この権限は、カスタム コード グループを使用する、あるいはアプリケーションの ClickOnce 配置マニフェストに権限を手動でリストすることで付与する必要があります。これ以外の解決策としては、クライアント アプリケーションとリソースの間に中間階層を導入し、この中間階層に、リソースへのアクセス処理を暫定的にカプセル化した形で処理させます。

同時実行管理とクローンの作成

ワーカー スレッドを生成してクライアントに対する内部処理を同時に実行する、トランザクショナル クライアントを想像してみてください。クライアントは、ワーカー スレッドの処理を独自のトランザクションにまとめたいと考えるでしょう。つまり、クライアントのトランザクションは、ワーカー スレッド上で行われた処理が一貫した場合にしか、コミットできません。しかしながら、このシナリオには問題が 2 つあります。1 つは、複数トランザクションの使用とマルチスレッディングという典型的な問題です。トランザクション内の同時処理を許可した場合、あるスレッドがトランザクションをコミットしようとして、別のスレッドが同じトランザクションをアボートしようとするといった状況が生じる可能性があります。2 つ目の問題は、クライアントのアンビエント トランザクションは TLS に格納されるのですが、その結果、アンビエント トランザクションはワーカー スレッドに対してプロパゲートできなくなります。System.Transactions には、こうしたトランザクションの同時処理に伴う問題を解決するための、ビルトイン サポートが組み込まれています。

Transaction クラスには、次のように定義された、DependentClone() というメソッドが用意されています。


Public enum DependentCloneOption
{
     BlockCommitUntilComplete,
     RollbackIfNotComplete,
}
[Serializable]
public class Transaction : IDisposable,ISerializable
{
     public DependentTransaction DependentClone(DependentCloneOption cloneOption);
     // 残りのメンバ
}
                

DependentClone() は、次のように定義されたシール付きクラスである DependentTransaction のインスタンスを返します。


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

DependentTransactionTransaction の派生クラスであり、その唯一の目的は、クローン作成されたトランザクションによる処理が完了し、コミットの準備ができたことを (おそらくはトランザクションの作成者に対して) 知らせることです。サンプル コード 15 は、従属トランザクションのクローンを作成し、それをワーカー スレッドに引き渡す処理を示しています。

サンプル コード 15. 別スレッドによる従属クローンの使用 Using dependent clone by another thread


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 DependentTransaction;
          Debug.Assert(dependentTransaction != null);
          Transaction oldTransaction = Transaction.Current;
          try
          {
               Transaction.Current = dependentTransaction;
          /* トランザクション処理を行います。 */
          dependentTransaction.Complete();
          }
          finally
          {
               dependentTransaction.Dispose();
          Transaction.Current = oldTransaction;
          }
     }
}
// クライアント コード
using(TransactionScope scope = new TransactionScope())
{
     Transaction currentTransaction = Transaction.Current;
     DependentTransaction dependentTransaction;
     dependentTransaction = 
currentTransaction.DependentClone(DependentCloneOption.BlockCommitUntilComplete);
     WorkerThread workerThread = new WorkerThread();
     workerThread.DoWork(dependentTransaction);
     /* 続いて、何かトランザクション処理を行います。 */
     scope.Complete();
}
                

クライアント コードは、アンビエント トランザクションの設定もするトランザクショナル スコープを作成します。アンビエント トランザクションはコミット可能トランザクションですが、このトランザクションをワーカー スレッドに引き渡すべきではありません。代わりに、現在のトランザクション上で DependentClone() を呼び出して、クライアントに現在の (アンビエント) トランザクションのクローンを作成させます。DependentClone() は従属トランザクション オブジェクトを生成します。ベースとなるリターン オブジェクトは DependentTransaction であり、CommittableTransaction ではありません。なお、Transaction クラスには、トランザクションの本物のクローン (CommittableTransaction (ただし該当する場合) 等) を返す、ロー メソッドの Clone() も用意されていますが、本セクションの冒頭に掲げた理由から、この危険なクローンをワーカー スレッドに引き渡さないようにしてください。

WorkerThread クラスには、新しいスレッド上で実行される ThreadMethod() が用意されています。クライアントは、従属トランザクションをスレッド メソッド パラメータとして渡す新規スレッドを開始します。問題は、完了の同期です。ワーカー スレッドの処理が終了する前に、クライアントがトランザクショナル スコープの最後に到達してしまったらどうなるでしょうか。そのとき、クライアントはどうすれば自分のトランザクションをコミットすることができるのでしょうか。

この問題を解消するために、トランザクション オブジェクトは自分が作成したすべての従属クローンをトラックします。DependentClone() メソッドは、DependentCloneOption という型名の enum 型で、cloneOption という名前のパラメータを取ります。cloneOptionDependentCloneOption.BlockCommitUntilComplete に一致している場合にクライアントがトランザクションをコミットしようとすると、そのクライアントはすべての従属トランザクションが完了するまでブロックされた状態になります。サンプル コード 15 では、トランザクション オブジェクトを破棄しようとしたクライアントが、using ステートメントの最後でブロックされています。cloneOptionDependentCloneOption.RollbackIfNotComplete の場合、クライアントがコミットしようとしていてもブロックされません。その代わり、アクティブな従属トランザクションがまだ存在しているのに、クライアントがコミットを実行しようとすると、トランザクションはアボートされ、TransactionAborted 例外が投げられます。それだけではなく、ワーカー スレッドは影響を受けないため、トランザクションを無駄に実行し続けることになります。カスタム手動同期を必要としない限り、cloneOption には常に DependentCloneOption.BlockCommitUntilComplete を設定するようにしてください。

cloneOptionDependentCloneOption.BlockCommitUntilComplete を設定していても、同時実行の問題がなくなるわけではありません。特に、次の 4 点については認識しておく必要があります。

  • ワーカー スレッドはトランザクションをロールバックしたが、クライアントはコミットしようとした場合、TransactionAborted 例外が投げられます。
  • ワーカー スレッドは Complete() を一度しか呼び出すことができません。それ以上呼び出すと、InvalidOperation 例外が発生します。
  • トランザクションでは、ワーカー スレッド 1 個に対し、1 個の新しい従属クローンが作成されるようにしてください。同じ従属クローンを複数のスレッドに渡してはいけません。Complete() を実行できるのは、いずれか 1 つのスレッドだけだからです。
  • ワーカー スレッドから新しいワーカー スレッドを生成した場合は、もとのワーカー スレッドの従属クローンをベースに新しい従属クローンを作成し、それを新しいスレッドに渡すようにしてください。

インターオペラビリティ (相互運用性)

System.Transactions は .NET Enterprise Services をネイティブにサポートしています。つまり、サービス コンポーネントであれば、特に目新しいことをしなくても、新しいトランザクション マネージャとトランザクション プロモーションを利用することができます (サンプル コード 2 参照)。

ここで興味深いクエスチョンです。トランザクショナル サービス コンポーネントが TransactionScope オブジェクトを生成したらどうなるでしょうか。あるいは、トランザクショナル スコープがサービス コンポーネントを作成した場合はどうでしょうか。TransactionScope オブジェクトが Enterprise Services トランザクショナル コンテキストで実行された場合、アンビエント トランザクションとして Enterprise Services トランザクションを使わなければならないのでしょうか。TransactionScope オブジェクトはどの Enterprise Services コンテキストを使用すればいいのでしょうか。また、上記のような、種々様々なものが組み合わさった状況ではどういった問題が生じるのでしょうか。Enterprise Services トランザクション プログラミング モデルは、オブジェクト ライフサイクルとステート管理が密接に関係しています。これを、オブジェクトベースではないトランザクショナル スコープと組み合わせるわけですから、副作用として思わぬ結果が生じることがあります。

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


public enum EnterpriseServicesInteropOption
{
     Automatic,
     Full,
     None
}
                

これにより、相互運用レベルを指定することができます。TransactionScope クラスには、EnterpriseServicesInteropOption を受け入れるコンストラクタがいくつか用意されています。以下はその一例です。


[PermissionSet(SecurityAction.LinkDemand, Name="FullTrust")]
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
               (TransactionScopeOption.Required, options, 
                EnterpriseServicesInteropOption.None) 
               )
          {
          // cope.Complete() への呼び出しはありませんが、
          // COM+ トランザクションはコミットができてしまいます。
          }
     }
}
                

EnterpriseServicesInteropOption.None は、Enterprise Services トランザクションとアンビエント トランザクションを一緒に使用した結果生じる「副作用」を排除してくれます。そのため、最も安全なオプションといえます。EnterpriseServicesInteropOption.None は、EnterpriseServicesInteropOption 値を受け入れないコンストラクタを使用する TransactionScope のデフォルトとして使われます。

EnterpriseServices トランザクションをどうしてもアンビエント トランザクションと一緒に使いたい場合は、EnterpriseServicesInteropOption.Automatic または EnterpriseServicesInteropOption.Full のいずれかを使用する必要があります。いずれの値もサービス ドメインを利用するため、Windows XP Service Pack 2 または Windows 2003 Server での動作が必須となります。EnterpriseServicesInteropOption.Full は、可能な限りサービス コンポーネントに近い動作をします。TransactionScope オブジェクトがトランザクションを必要とする場合 (既存のアンビエント トランザクションに参加する、あるいは新規トランザクションを作成する場合)、EnterpriseServicesInteropOption.Full は、新規で Enterprise Services トランザクショナル コンテキストを作成し、それを既存の Enterprise Services トランザクションに投入するので (あるいは新規で Enterprise Services トランザクションを作成します)、使用されるアンビエント トランザクションは、Enterprise Services コンテキストで使用されるものと同じトランザクションになります。TransactionScope オブジェクトがトランザクションを必要としない場合、スコープはデフォルトの Enterprise Services コンテキストに配置されます。

EnterpriseServicesInteropOption.Automatic は、EnterpriseServicesInteropOption.NoneEnterpriseServicesInteropOption.Full が合わさった動きをします。EnterpriseServicesInteropOption.Automatic は、スコープがデフォルトの Enterprise Services コンテキストに構築されているか否かをチェックします。

デフォルトの Enterprise Services コンテキストに構築されていれば、Enterprise Services トランザクションは存在しません。EnterpriseServicesInteropOption.Automatic は、EnterpriseServicesInteropOption.None と同様の動作をし、新しい Enterprise Services コンテキストは作成しません。スコープがデフォルト コンテキスト以外の Enterprise Services コンテキストに構築されている場合は、EnterpriseServicesInteropOption.AutomaticEnterpriseServicesInteropOption.Full と同様の動作をします。

EnterpriseServicesInteropOption.Automatic には欠点があると思っています。本来であれば、デフォルトのコンテキストかどうかを検証するのではなく、作成するコンテキストがトランザクショナルか否かをチェックすべきです。

下の表 2 と表 3 は、トランザクションを必要とする TransactionScope オブジェクトで使用する、Enterprise Services コンテキストとアンビエント トランザクションを示したものです。

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

ES コンテキストNoneAutomaticFull
デフォルト コンテキストデフォルト コンテキストデフォルト コンテキスト新規トランザクショナル コンテキストを作成
非デフォルト コンテキストクライアントのコンテキストを保持新規トランザクショナル コンテキストを作成新規トランザクショナル コンテキストを作成

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

ES コンテキストNoneAutomaticFull
デフォルト コンテキストSTSTES
非デフォルト コンテキストSTESES
ST - スコープのアンビエント トランザクションは
System.Transactions
に管理され、Enterprise Services コンテキストのトランザクション (存在する場合) からは独立しています。
ES - スコープのアンビエント トランザクションは、Enterprise Services コンテキストのトランザクションと同じになります。

まとめ

System.Transactions は、革新的かつ実用的な、待望のアーキテクチャです。System.Transactions のおかげで、アプリケーションプログラミング モデルとトランザクション管理を分離することができ、トランザクション マネージャの自動プロモーションを可能にして、パフォーマンスの最適化を実現します。また、将来のトランザクション マネージャにも対応できる、拡張性のあるアーキテクチャを持ちます。次期リリースの System.Transactions では、分離ロックやリカバリ マネージメントが提供されるため、揮発性リソース マネージャなど、カスタム リソース マネージャの構築が今まで以上に容易になります。Indigo といったテクノロジーや Longhorn のようなプラットフォームは、一貫したトランザクション管理を実現する基盤として System.Transactions に依存し、プラガブルなプロバイダ モデルで新しいトランザクション マネージャを提供する予定です。System.Transactions は .NET Framework v2.0 が最初のリリースですが、堅牢かつハイパフォーマンスなエンタープライズ アプリケーションを構築する上で、大きな力となる新機能が開発環境に加わったといえるでしょう。

著者について

Juval Lowy はソフトウェア アーキテクトで、.NET アーキテクチャ コンサルティングと .NET の上級者向けトレーニングを専門とする、IDesign 社の社長を務めます。Juval は、シリコン バレー地域の Microsoft Regional Director を務め、マイクロソフトと共に .NET の普及に尽力しています。近著には、「Programming .NET Components, Second Edition」(O'Reilly 発行) があります。また、マイクロソフトでは、.NET の将来のバージョンの内部デザイン レビューに参加しており、.NET 開発のあらゆる側面に関する 記事 を多数発表しています。また、デベロップメント カンファレンス ではしばしばプレゼンターを務めています。マイクロソフトは、彼を Software Legend - 世界でもトップ .NET エキスパートおよび業界リーダーの一人であると認めています。


© 2009 Microsoft Corporation. All rights reserved. 使用条件  |  商標  |  プライバシー
Page view tracker