テーブルアダプタと TransactionScope の組み合わせ
さて、Silverlight 2 や WCF などの最新テクノロジの話ばっかりここまで書いてきたので、たまには地味(けれどもめちゃめちゃ重要)な話をひとつ書いてみたりします。結論を先に書くと、以下の通りです。
「SQL Server 2008 と .NET Framework 2.0 SP1 を使うと、テーブルアダプタと TransactionScope を組み合わせる際の昇格条件が緩和される(=MS-DTC 昇格せずに済むことがある)」
具体的には、以下のようなコードで MS-DTC 昇格が発生しなくなります。
using (TransactionScope scope = new TransactionScope(TransactionScopeOption.RequiresNew))
{
PubsDataSetTableAdapters.QueriesTableAdapter ta = new PubsDataSetTableAdapters.QueriesTableAdapter();
// SELECT price FROM titles WITH (UPDLOCK) WHERE title_id=@title_id
decimal? price = ta.GetPriceByTitleIdWithUpdlock("BU1032");
decimal newPrice = price.Value + 1;
// UPDATE titles SET price=@newPrice WHERE title_id=@title_id
ta.SetNewPriceByTitleId(newPrice, "BU1032");
scope.Complete();
}
これがどれぐらい大きなインパクトなのかは、テーブルアダプタと TransactionScope に慣れ親しんでいる方であればすぐにご想像がつくかと思うのですが(というかこの機能をどれほど待ち望んだことか...!)、あまりご存じない方もいらっしゃると思いますので、以下に少し詳しく説明します。
- 自動トランザクション(TransactionScope)と MS-DTC の関係について
- 自動トランザクション(TransactionScope)における昇格動作(Promotion 動作)について
- テーブルアダプタと TransactionScope を組み合わせた場合の動作について
- SQL Server 2008 と .NET Framework 2.0 SP1 を使った場合について
※ なお、自動トランザクション(TransactionScope)とテーブルアダプタの基本についてはここでは解説しませんので、拙著(Visual Studio 2005 による Web アプリケーション構築技法)あたりを読んでみていただけると助かります。
[自動トランザクション(TransactionScope)と MS-DTC の関係について]
TransactionScope は、コネクションの外側からトランザクションを制御するための技術で、これを使うと、複数の物理接続から行われたデータベース読み書きを、ひとつのトランザクションに束ねることができます。
具体的な使い方も非常に簡単で、下図に示すように、TransactionScope を使った using ブロックの内側で、「個々にデータベース接続を開き、データを読み書きし、接続を閉じる」という処理を書くだけです。このようにすると、個々のデータベースに対する読み書きをひとつのトランザクションに束ねることができました。
一般的に、このような「複数のデータベースに対する読み書き処理」をひとつのトランザクションに束ねるためには、2 相コミット(2 Phase Commit, 以下では 2PC と略します)と呼ばれる制御が必要です。この 2 相コミット制御を行っているのが、Windows OS のサービスの一つである MS-DTC(Microsoft Distributed Transaction Coordinator)です。TransactionScope オブジェクトを使うと、このオブジェクトが MS-DTC サービスとの連携動作を行い、MS-DTC サービスがこの 2 相コミット制御を行ってくれます。
さて、この自動トランザクション(TransactionScope)という技術は、以下の 2 つの観点から重要です。
- 複数のデータベースサーバに対する読み書きを、まとめてコミットするための技術。
- コネクションの外側からトランザクション制御を行うことで、コンポーネント分割の自由度を高めるための技術。
ここで問題になるのは、後者の目的で TransactionScope を使う場合です。例えば、ビジネスロジッククラス(BC)の中からデータアクセスクラス(DAC)を切り出そうと思う際には、TransactionScope を使うと、下図のように非常に簡単に DAC を切り出すことができます。
ところが、自動トランザクションの内部制御には MS-DTC が使われているため、上図のように、スコープ内で一つのデータベースしか利用していない場合でも MS-DTC が動作してしまい、不要なオーバヘッドが発生してしまう、という問題があります。
[自動トランザクション(TransactionScope)における昇格動作(Promotion 動作)について]
こうした問題を避けるため、(.NET Framework 2.0 と SQL Server 2005 では)、以下の 2 つの条件を満たす場合には、TransactionScope を使っても MS-DTC が動作しないようになっていました。
- 対象となるデータベースが、SQL Server 2005 であること。
- TransactionScope 内で一つの物理コネクションのみを使うこと。
この動作は、.NET Framework 2.0 と SQL Server 2005 の 2 つの連携動作によって実現されており、その肝になっているのが以下の 2 つでした。
- LCT (Lightweight Commitable Transaction)
- 昇格動作(Promotion)
もう少し詳しく説明すると、以下のようになります。
- まず、SQL Server 2005 に対する最初のコネクションによる SQL 処理は、後から昇格可能な LCT (Lightweight Commitable Transaction)と呼ばれるトランザクションにより処理されます。(このまま TransactionScope を終えると、MS-DTC は動作せず、通常のローカルトランザクションと同様の仕組みにより処理されることになります=パフォーマンスオーバヘッドがほとんどありません)
- その後、さらに新規にコネクションが開かれると、当該 LCT は自動的に分散トランザクション(MS-DTC を使うトランザクション)に昇格します。
つまり、TransactionScope を使う場合は、最初から MS-DTC を使うことが確定しているわけではなく、「とりあえず MS-DTC を使わないですむのならがんばってみる」という挙動をし、ダメだとわかった時点で MS-DTC を使う分散トランザクションに昇格する、という動作をします。(このため、TransactionScope は Promotable Transaction (昇格可能トランザクション)とも呼ばれています。)
[テーブルアダプタと TransactionScope を組み合わせた場合の動作について]
がしかし、現実的な話をすると、前述のような昇格動作機能は事実上宝の持ち腐れ、というのが実際のところでした。というのも、実際の開発現場では、TransactionScope は Visual Studio 2005 でサポートされたテーブルアダプタと組み合わせて使われることが多く、その場合には、物理コネクションが一本で済むということが事実上なかったからです。
このことを具体的に示すために、ちょっとしたサンプルアプリケーションを作ってみたいと思います。(例によって pubs.mdf ファイルはどっかから入手してください。)
- まず、新規に C# の ConsoleApplication を 1 つ作成します。(.NET Framework 3.5)
- プロジェクトに pubs.mdf ファイルを貼り付けます。(自動起動するウィザードはいったんキャンセル)
- PubsDataSet.xsd ファイルを追加します。
- データセットデザイナ上に、ツールボックスから "Query" をドラッグ&ドロップで追加します。
TableAdapter クエリの構成ウィザードが開いたら、以下の作業を行います。
- "SELECT price FROM titles WITH (UPDLOCK) WHERE title_id=@title_id" (単一の値を返す SELECT 文)を "GetPriceByTitleIdWithUpdlock" で作成。
- "UPDATE titles SET price=@newPrice WHERE title_id=@title_id" (UPDATE 文)を "SetNewPriceByTitleId" で作成。
さらに Program.cs ファイルに戻り、以下の作業を行います。
- 参照設定の追加を行い、"System.Transactions.dll" への参照を追加。
- コードの先頭に using System.Transactions; を追加。
- 以下のようなコードを追加。
using (TransactionScope scope = new TransactionScope(TransactionScopeOption.RequiresNew))
{
PubsDataSetTableAdapters.QueriesTableAdapter ta = new PubsDataSetTableAdapters.QueriesTableAdapter();
decimal? price = ta.GetPriceByTitleIdWithUpdlock("BU1032");
decimal newPrice = price.Value + 1;
ta.SetNewPriceByTitleId(newPrice, "BU1032");
Console.WriteLine("価格を更新しました。" + newPrice.ToString());
scope.Complete();
}
できあがったらこれを Ctrl + F5 で動作確認します。
さて、これだけだと MS-DTC が動作したかどうかがわからないので、以下の 2 つの作業を行います。
- DTC 昇格が発生した場合のイベントハンドラの追加。 (TransactionManager クラスの DistributedTransactionStarted イベントハンドラを記述する。このようにすると、DTC 昇格が発生した際に処理を行うことができます。ちなみに DTC 昇格させるのがイヤな場合には、このイベントハンドラの中で例外を発生させれば、DTC 昇格を起こさせないという制御もできます。)
- MS-DTC のモニタツールの起動。(MMC を起動し、スナップインとして「コンポーネントサービス」を追加してください。)
static void Main(string[] args)
{
TransactionManager.DistributedTransactionStarted += new TransactionStartedEventHandler(TransactionManager_DistributedTransactionStarted);
using (TransactionScope scope = new TransactionScope(TransactionScopeOption.RequiresNew))
{
PubsDataSetTableAdapters.QueriesTableAdapter ta = new PubsDataSetTableAdapters.QueriesTableAdapter();
decimal? price = ta.GetPriceByTitleIdWithUpdlock("BU1032");
decimal newPrice = price.Value + 1;
ta.SetNewPriceByTitleId(newPrice, "BU1032");
Console.WriteLine("価格を更新しました。" + newPrice.ToString());
scope.Complete();
}
}
static void TransactionManager_DistributedTransactionStarted(object sender, TransactionEventArgs e)
{
Console.WriteLine("MS-DTC トランザクションへの昇格が発生しました。");
}
この状態でアプリケーションを実行すると、MS-DTC が動作している様子が確認できます。MS-DTC が動作するのは、以下のような理由によります。
- Visual Studio 2008 に添付されている SQL Server Express Edition が 2005 版である。
- TransactionScope 内でテーブルアダプタの GetPriceByTitleIdWithUpdlock(), SetNewPriceByTitleId() メソッドを順次叩いているが、その内部では、それぞれ「コネクションのオープン → SQL 文実行 → コネクションのクローズ」が行われている。
- 結果として、2 回別々にコネクションを開くことになるため、MS-DTC 昇格が発生する。
[SQL Server 2008 と .NET Framework 2.0 SP1 を使った場合について]
しかし、前述のアプリケーションコードを考えた場合、物理的には同一のデータベースを利用しているわけですから、MS-DTC による分散トランザクション処理ではなく、LCT によるローカルトランザクション処理を行ってほしい、と思うのが人情でしょう。で、これが SQL Server 2008 と .NET Framework 2.0 SP1 を使うことにより可能になりました。
つまり、前述のアプリケーションを SQL Server 2008 に対して使うと、下記のように DTC 昇格が発生しません(=ローカルトランザクションと同等のパフォーマンスで処理されます) 。(接続文字列を書き替えて SQL Server 2008 に対して処理を行うようにするか、または SQL Server 2005 Express Edition を SQL Server 2008 Express Edition にアップグレードしてください。)
このような挙動を行えるようになった詳細な理由は、以下にまとめられています。
Extending Lightweight Transactions in SqlClient
が、読むのは大変だと思うので、キーポイントをまとめると以下のようになります。
SQL Server 2008 側で、特殊な「コネクションリセットモード」がサポートされるようになった。
(トランザクションをロールバックせずにコネクションをリセットするというモードがサポートされた)
これにより、「.Close() でいったんプールにもどした物理コネクションを再度 .Open() する際に、新規の物理コネクションを開かなくても済む」ようになった。
結果として、物理コネクションがスコープ内できちんと再利用されるため、前述のようなコードでの昇格処理が不要になった。(=MS-DTC が動作しない)
この機能強化は、地味ですが極めて重要です。というのも、テーブルアダプタと TransactionScope を使う開発は極めて高開発生産性・高保守性なのですが、今までは MS-DTC が動作してしまう=性能的なオーバヘッドがある、というデメリットで敬遠する人がいました。しかし、SQL Server 2008 と .NET Framework 2.0 SP1 を使っていただくと、この問題が回避できることになります。
※ 実際には MS-DTC のオーバヘッドはそれほど大きくなく、取引システムなど特に性能を重視するようなアプリケーションでなければまず問題になることはないのですが、「ちょっとでも性能が悪くなる」という理由で極端に嫌うような人がいたことも事実です。。。
なお、本機能について、いくつか注意事項をまとめておきます。
この機能は、SQL Server 2008 と .NET Framework 2.0 SP1 を組み合わせる場合のみで使えます。SQL 2005 + .NET 2.0 SP1 や、SQL 2008 + .NET 2.0 (SPなし) などでは動作しません。(=これらのケースでは MS-DTC 昇格動作が発生します。)
この機能は、データベースが単一であっても物理コネクションを同時に複数開く場合には使えません。例えば、以下のようなコードでは MS-DTC 昇格が発生します。
using (TransactionScope scope = new TransactionScope(...))
{
SqlConnection sqlcon1 = new SqlConnection("...");
SqlConnection sqlcon2 = new SqlConnection("...(上と同じ接続文字列)");
sqlcon1.Opne();
// 何か SQL 処理を実行
// sqlcon1 を Close() する前に sqlcon2 を Open() すると、物理接続が 2 本=DTC 昇格
sqlcon2.Open();
...
}
この機能は、Visual Studio 2005 & .NET Framework 2.0 で自動トランザクションとテーブルアダプタが出た当初から待望していたのですが、ようやくこうした形でサポートされるようになったのはうれしい限りです。これがあると、テーブルアダプタ+自動トランザクションの組み合わせによる開発方法の適用範囲が今まで以上に増えるでしょうね。
[2009/01/23 追記]
本件ですが、MSDN Library に情報が出ましたので追記しておきます。興味がある方はこちらをどうぞ。
System.Transactions Integration with SQL Server (ADO.NET)
Comments
Anonymous
May 19, 2009
こんなアップデートがちゃっかり行われていたんですね。半年以上も知りませんでした! 分散トランへの昇格は、大量のDMLを送りつける夜間バッチなどではちょっとした懸念事項となる場合がありましたので、緩和されるのであれば非常にありがたい機能です。 ただ、「結果として、物理コネクションがスコープ内できちんと再利用されるため・・・」とあるのですが、ASP.NETみたいな環境の場合、コネクションプールに一旦戻ったコネクションが、同じトランザクションスコープ内で同じ物理コネクションが再利用される保障って無い(他のリクエストで発生したトランザクションが利用してしまう)ように思えるのですが、この辺も適切に処理されるということなのでしょうか?Anonymous
September 30, 2009
がんふぃーるどさん、こんにちは。リプライがかなり遅れてごめんなさい。 > ASP.NETみたいな環境の場合、コネクションプールに一旦戻ったコネクションが、同じトランザクションスコープ内で同じ物理コネクションが再利用される保障って無い(他のリクエストで発生したトランザクションが利用してしまう)ように思えるのですが、この辺も適切に処理されるということなのでしょうか? はい、というかそれができなければまずいわけで;。 見たところ、MSDN のドキュメントがかなり細かく update されているのでこれを参照するとよいかと思います。→ http://msdn.microsoft.com/en-us/library/ms172070.aspx > 分散トランへの昇格は、大量のDMLを送りつける夜間バッチなどではちょっとした懸念事項となる場合がありましたので、緩和されるのであれば非常にありがたい機能です。 夜間バッチのような大量 DML 処理の場合には、実際には極力 .Open()/.Close() しない方がよいのですよねぇ。というのも、.Open() 処理を行った場合、プールから取り出した物理コネクションに対してリセット処理が走るため、物理コネクションを開くような大きなオーバヘッドはないものの、小さなオーバヘッドはやはりあるからです。このため、例えば 1 行単位に似たような UPDATE 処理を数万件繰り返すような処理の場合には、TableAdapter を使って数万回更新するよりも、ADO.NET ライブラリを直接いじって、コネクションを開きっぱなしにしてパラメタライズドクエリを数万回実行する(.ExecuteNonQuery())方が、性能はかなりよくなります。(物理コネクションのリセット処理自体は小さなものですが、ちりも積もれば山となりますからねぇ;。)