Skip to content

MS_TransactionScope

nishi_74322014 edited this page Aug 13, 2026 · 1 revision

TransactionScope

概要

  • TransactionScope クラスは、2 相コミットを実現するための .NET 2.0 の API であり、
    内部的には、トランザクション マネージャ(TM)であるMS-DTCを使用している。

  • MS-DTCだけでなく、WS-Transaction(WCF)などで使われる予定だったが、
    最近、これらの技術適用は非常に少なくなった。

補足(昇格:Lightweight Transaction Manager): TransactionScope
常に MS-DTC を使うわけではない。
System.Transactions の LTM(Lightweight Transaction Manager)が
関与するリソースの数に応じて段階的に昇格する。

段階 状況 コスト
ローカル トランザクション 単一の接続のみ 低(通常の BEGIN TRAN と同等)
分散トランザクション 2 つ目のリソースが参加した時点で MS-DTC へ昇格 高(2 相コミット、in-doubt リスク)

注意すべきは、同じ DB への接続でも接続を 2 つ開けば昇格しうる点。
SQL Server 2008 以降は同一 DB への複数接続なら
昇格を回避できるケースがあるが、環境差が大きい。
「意図せず MS-DTC が有効化を要求されて落ちる」というのは典型的な事故で、
接続は 1 つに束ねるのが基本的な回避策である。

補足(最新化:.NET Core 以降の制約):

ランタイム MS-DTC への昇格
.NET Framework サポート
.NET Core 〜 .NET 6 未サポート(昇格時に PlatformNotSupportedException
.NET 7 以降 Windows のみサポート(Linux は未サポート)

なお、TransactionScope そのもの(ローカル トランザクション止まりの利用)は
.NET Core 以降でも動作する。問題になるのは昇格が発生する場合だけである。

詳細

経緯

  • ASP.NET 1.x では、

    • MS-DTCは Enterprise Services 上からの利用に限られたが、
      Enterprise Services を使用した場合、
      ビジネス プロセス・ビジネス コンポーネントなどのアプリケーション ブロックを
      Enterprise Services 上に切り出す必要があった。
    • また、Enterprise Services アプリケーション開発は難易度が高く、
      あまり活用されなかった。
  • このため、ASP.NET 2.0 からは、

    • 上記の TransactionScope クラスを利用することで、Enterprise Services などの
      中間層を用いないで、ASP.NET 上から簡単に 2 相コミットを利用できるように
      改善が図られた(Windows DNA(MS-DTC)の ASP 上から
      MS-DTCを利用する方式に近い)。
    • なお、TransactionScope クラスは、WCF
      Bindingがサポートする
      WS-AtomicTransaction でもインターフェイスの定義がされたが、
      こちらの実装は、ほぼ実用化に至らず、
      その後、WS-I 等の標準化団体も解散してしまった。

動作確認用のサンプル プログラム(ASP.NET)

2 相コミットの動作確認用サンプル プログラムを用いて、
TransactionScope クラスの利用方法を説明する。

SQL Server

下記は、SQL Server 用のデータ アクセス ライブラリである、
SqlClient を利用した場合のサンプル コード。

private void Two_Phase_Commit(bool flag)
{
  TransactionOptions txopt = new TransactionOptions();
  txopt.IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted;

  //txopt.IsolationLevel = System.Transactions.IsolationLevel.RepeatableRead;
  //txopt.IsolationLevel = System.Transactions.IsolationLevel.Serializable;
  //txopt.IsolationLevel = System.Transactions.IsolationLevel.Snapshot;
  //txopt.IsolationLevel = System.Transactions.IsolationLevel.ReadUncommitted;

  using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required, txopt))
  {
    using (SqlConnection con = new SqlConnection(@"Data Source=AAA;Initial Catalog=northwind;User ID=xxx;Password=xxx;"))
    {
      using (SqlCommand com = new SqlCommand())
      {
        com.Connection = con;
        com.CommandText = "insert into table1(bbb) values('データ')";
        try
        {
          con.Open();
          com.ExecuteNonQuery();
        }
        finally
        {
          con.Close();
        }
      }
    }
    using (SqlConnection con = new SqlConnection(@"Data Source=BBB;Initial Catalog=northwind;User ID=xxx;Password=xxx;"))
    {
      using (SqlCommand com = new SqlCommand())
      {
        com.Connection = con;
        com.CommandText = "insert into table1(bbb) values('データ')";
        try
        {
          con.Open();
          com.ExecuteNonQuery();
        }
        finally
        {
          con.Close();
        }
      }
    }

    if (flag) // flag変数はコミット、ロールバックのテストのための実装
    {
      scope.Complete();
    }
    else{ }
  }
}
  • 分離レベルの選択が可能。Snapshot 分離レベルは、SQL Server 2005 以降でサポートされる。
  • using (TransactionScope scope の Using ステートメント範囲内の
    TransactionScope オブジェクトの破棄までがトランザクション範囲となる
    (Using ステートメント コードブロックは、範囲外に出る際に、
    自動的にオブジェクトの Dispose() メソッドを呼び出す仕様になっている)。
  • このため、Using ステートメントを使用しない実装をする際は
    手動で TransactionScope.Dispose() メソッドを呼び出す必要がある。
  • 最後に、TransactionScope.Complete() メソッドを呼び出し、
    上記のプログラム中の 2 つの接続が持つトランザクションは
    分散トランザクションとして 2 相コミットされる。
  • TransactionScope.Complete() メソッドを呼び出さない状態で
    TransactionScope オブジェクトが破棄された(Using ステートメントは、範囲外に出た)
    場合は、分散トランザクションは、ロールバックされる。

補足(最新化:現在書くならこう): 上記のサンプルは .NET 2.0 当時のもので、
現在の実装では以下の 3 点を必ず押さえる。

  1. TransactionScopeAsyncFlowOption.Enabled を指定する
    これを付けないと await を挟んだ時点でアンビエント トランザクションが
    引き継がれず、トランザクション外で実行されるという深刻なバグになる。
  2. 既定の分離レベルは Serializable
    TransactionOptions を省略すると Serializable になり、
    ロック競合が激増する。上のサンプルのように明示指定すること。
  3. SQL は必ずパラメータ化する
    元のサンプルはリテラル埋め込みだが、実務ではパラメータを使う。
var txopt = new TransactionOptions
{
    IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted,
    Timeout = TimeSpan.FromSeconds(30),
};

using (var scope = new TransactionScope(
    TransactionScopeOption.Required, txopt,
    TransactionScopeAsyncFlowOption.Enabled))
{
    await using (var cn = new SqlConnection(cs))
    {
        await cn.OpenAsync();
        await using var cmd = new SqlCommand(
            "insert into table1(bbb) values(@v)", cn);
        cmd.Parameters.Add("@v", SqlDbType.NVarChar, 100).Value = "データ";
        await cmd.ExecuteNonQueryAsync();
    }
    scope.Complete();
}

補足(TransactionScopeOption の意味):

挙動
Required 既存のアンビエント トランザクションに参加。無ければ新規作成(既定)
RequiresNew 常に新しいトランザクションを開始(外側とは独立してコミット/ロールバック)
Suppress トランザクションの外で実行する(監査ログの記録など、ロールバックさせたくない処理に使う)

Oracle

  • 下記は、Oracle 用のデータ アクセス ライブラリである、
    System.Data.OracleClient、ODP.NET を利用した場合のサンプル コードである。
  • 実装を見てわかるとおり、ADO.NET の仕様に合わせ、
    API のインターフェイスも共通化されているため、
    他の DBMS のデータプロバイダも含め、実装に大きな違いは無い。
private void Two_Phase_Commit(bool flag)
{
  TransactionOptions txopt = new TransactionOptions();
  txopt.IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted;

  //txopt.IsolationLevel = System.Transactions.IsolationLevel.RepeatableRead;
  //txopt.IsolationLevel = System.Transactions.IsolationLevel.Serializable;
  //txopt.IsolationLevel = System.Transactions.IsolationLevel.Snapshot;
  //txopt.IsolationLevel = System.Transactions.IsolationLevel.ReadUncommitted;

  using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required, txopt))
  {
    using (OracleConnection con = new OracleConnection(@"User Id=xxx;Password=xxx;Data Source=AAA/orcl;"))
    {
      using (OracleCommand com = new OracleCommand())
      {
        com.Connection = con;
        com.CommandText = "insert into table1(aaa, bbb) values(1, 'データ')";
        try
        {
          con.Open();
          com.ExecuteNonQuery();
        }
        finally
        {
          con.Close();
        }
      }
    }
    using (OracleConnection con = new OracleConnection(@"User Id=xxx;Password=xxx;Data Source=BBB/orcl;"))
    {
      using (OracleCommand com = new OracleCommand())
      {
        com.Connection = con;
        com.CommandText = "insert into table1(aaa, bbb) values(1, 'データ')";
        try
        {
          con.Open();
          com.ExecuteNonQuery();
        }
        finally
        {
          con.Close();
        }
      }
    }

    if (flag) // flag変数はコミット、ロールバックのテストのための実装
    {
      scope.Complete();
    }
    else{ }
  }
}

移行メモ(元コードの体裁): 元ページの Oracle 側サンプルは
finally ブロックの括弧の対応が崩れていたため、
SQL Server 側のサンプルに合わせて整形した(処理内容は変えていない)。

補足(分散トランザクションを避ける設計): 2 相コミットは
in-doubt トランザクションによる長時間ロックのリスクを伴うため、
現在は以下のいずれかで回避するのが定石。

  • 1 つの DB に集約して単一のローカル トランザクションで済ませる
  • Outbox パターン: 業務データの更新とメッセージの記録を
    同一 DB・同一トランザクションで確定し、別プロセスが配送する
  • Saga パターン: 各サービスがローカルにコミットし、
    失敗時は補償トランザクションで打ち消す

詳細はクラウド設計パターンを参照。


Tags: 移行, インフラストラクチャ, Windows, データアクセス, ADO.NET

NetDevInfraWiki

マイクロソフト系技術情報 Wiki
Open 棟梁 Wiki

(未着手)

開発基盤部会 Wiki

移行管理: DONETODO

Clone this wiki locally