ダーティ リードとその解決方法 その 1

(目次: トランザクションのサンプル)

ダーティ リード (Dirty Read) とは、更新中でコミットされていないデータを読み取ってしまう現象のことです。
今回は、ダーティ リードの発生と SQL Server における解決方法について、実際のコードを示して説明します。

前提として、SQL Server におけるロック メカニズムやトランザクション分離レベルについての基礎知識が必要になります。
これらは SQL Server のロック管理 (@IT) で確認できます。

以下で示すサンプルでは同時実行制御のために自動トランザクション (TransactionScope クラス) を使用していますが、
手動トランザクションなどを使用しても同様です。
さらに、データ アクセスのために SqlCommand クラスを使用していますが、
型指定された TableAdapter などを使用しても同様です。

■ ステップ 1: Read Uncommitted (→ダーティ リード発生)

先にコードを示します。
コンソール アプリケーション プロジェクトを作成し、Program.cs に次のように記述します。
System.Transactions.dll への参照も必要です。


using System;
using System.Data.SqlClient;
using System.Threading.Tasks;
using System.Transactions;

namespace DirtyRead

    class Program 
    { 
        private const string NorthwindConnectionString = @"Data Source=.\SQLExpress;Initial Catalog=Northwind;Integrated Security=True"

        private const string SelectCommandText = "select UnitsInStock from Products where ProductID = 1"
        private const string UpdateCommandText = "update Products set UnitsInStock = 1 where ProductID = 1"

        private static readonly TransactionOptions transactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.ReadUncommitted }; 

        private static int hasStockCount; 

        static void Main(string[] args) 
        { 
            // 更新と読み取りを並列に実行します。 
            Parallel.Invoke( 
                () => 
                { 
                    for (int i = 0; i < 200; i++
                    { 
                        UpdateUnitsInStock(); 
                    } 
                }, 
                () => 
                { 
                    for (int i = 0; i < 100; i++
                    { 
                        SelectUnitsInStock(); 
                    } 
                } 
            ); 
        } 

        /// <summary> 
        /// Northwind データベースの Products テーブルの UnitsInStock の値を更新してロールバックします。 
        /// </summary> 
        private static void UpdateUnitsInStock() 
        { 
            using (var scope = new TransactionScope(TransactionScopeOption.Required, transactionOptions)) 
            using (var connection = new SqlConnection(NorthwindConnectionString)) 
            { 
                // データベース接続を開きます。 
                connection.Open(); 

                // 在庫個数を更新します。 
                using (var command = new SqlCommand(UpdateCommandText, connection)) 
                { 
                    command.ExecuteNonQuery(); 
                } 

                // TransactionScope.Complete メソッドを呼び出さないのでロールバックされます。 
            } 
        } 

        /// <summary> 
        /// Northwind データベースの Products テーブルの UnitsInStock の値を読み取ります。 
        /// </summary> 
        private static void SelectUnitsInStock() 
        { 
            using (var scope = new TransactionScope(TransactionScopeOption.Required, transactionOptions)) 
            using (var connection = new SqlConnection(NorthwindConnectionString)) 
            { 
                // データベース接続を開きます。 
                connection.Open(); 

                // 在庫個数を読み取ります。 
                using (var command = new SqlCommand(SelectCommandText, connection)) 
                { 
                    bool hasStock = (short)command.ExecuteScalar() > 0

                    if (hasStock) 
                    { 
                        hasStockCount++
                        Console.WriteLine("在庫あり ({0} 回目)", hasStockCount); 
                    } 
                    else 
                    { 
                        Console.WriteLine("在庫なし"); 
                    } 
                } 
            } 
        } 
    }
}


今回も Northwind データベースの Products テーブルを使用します。
読み取りおよび更新の対象となるのはこの図の値です。あらかじめ値を 0 に設定しておきます。 
なお、UnitsInStock 列のデータ型は smallint (C# では short) です。

Products テーブルのデータ

UpdateUnitsInStock メソッドは、その在庫個数の値を 1 に更新するものの、コミットせずにロールバックします。

SelectUnitsInStock メソッドは、その在庫個数の値を読み取って、在庫の有無を判定します。
トランザクション分離レベルを Read Uncommitted に設定して、一連の処理を TransactionScope で囲みます。

そしてこの 2 つのメソッドを並列に実行します。
なお、並列処理には .NET Framework 4 で追加されたタスク並列ライブラリ (TPL) を使用しています。

ステップ 1 実行結果

実行結果は上の図のようになります。
UpdateUnitsInStock メソッドの処理は実質的には無意味で在庫は増えていないはずですが、
在庫個数を 100 回読み取って 40 回ほどは「在庫あり」と判定されています (結果は毎回変化します)。
これがダーティ リードという現象です。

Read Uncommitted の場合には、データを読み取るときにロックを取得しないためにこのようなことが起こります。

■ ステップ 2: Read Committed (→解決)

トランザクション分離レベルを Read Committed に変更します。


private static readonly TransactionOptions transactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted };


コードを上記のように 1 行だけ書き換えて実行します。

ステップ 2 実行結果

すると、今度は期待通りに動作します。
Read Committed であればデータを読み取るときにロックを取得するため、
更新処理がコミットまたはロールバックされるまではデータを読み取ることができなくなります。

データの参照と更新の必要な業務で Read Uncommitted を指定することは慣習的にもまずないでしょう。

なお、ダーティ リードとその解決方法 その 2 に ADO.NET Entity Framework を利用したコードを載せました。

バージョン情報
.NET Framework 4
SQL Server 2008, 2008 R2

参照
.NET エンタープライズ Web アプリケーション開発技術大全 〈Vol.5〉 トランザクション設計編
エンタープライズ技術大全(トランザクション設計編) – WikiWiki (上記書籍の要約)
SQL Server のロック管理 (@IT)