web-dev-qa-db-ja.com

SQL Server:プールされた接続間での分離レベルのリーク

以前のスタックオーバーフローの質問( TransactionScope and Connection Pooling および How SqlConnection is IsolationLevel? )で示されているように、トランザクション分離レベルは、SQL ServerとADO.NETのプールされた接続全体でリークします。 (ADO.NETの上に構築されているため、System.TransactionsおよびEFも)。

つまり、どのアプリケーションでも、次の危険な一連のイベントが発生する可能性があります。

  1. データの一貫性を確保するために明示的なトランザクションを必要とする要求が発生した
  2. 重要ではない読み取りのみを実行しているため、明示的なトランザクションを使用しない他の要求が発生します。このリクエストはシリアライズ可能として実行され、潜在的に危険なブロッキングとデッドロックを引き起こす可能性があります

質問:このシナリオを防ぐ最善の方法は何ですか?今どこでも明示的なトランザクションを使用することが本当に必要ですか?

これは自己完結型の再現です。 3番目のクエリが2番目のクエリからSerializableレベルを継承していることがわかります。

class Program
{
    static void Main(string[] args)
    {
        RunTest(null);
        RunTest(IsolationLevel.Serializable);
        RunTest(null);
        Console.ReadKey();
    }

    static void RunTest(IsolationLevel? isolationLevel)
    {
        using (var tran = isolationLevel == null ? null : new TransactionScope(0, new TransactionOptions() { IsolationLevel = isolationLevel.Value }))
        using (var conn = new SqlConnection("Data Source=(local); Integrated Security=true; Initial Catalog=master;"))
        {
            conn.Open();

            var cmd = new SqlCommand(@"
select         
        case transaction_isolation_level 
            WHEN 0 THEN 'Unspecified' 
            WHEN 1 THEN 'ReadUncommitted' 
            WHEN 2 THEN 'ReadCommitted' 
            WHEN 3 THEN 'RepeatableRead' 
            WHEN 4 THEN 'Serializable' 
            WHEN 5 THEN 'Snapshot' 
        end as lvl, @@SPID
     from sys.dm_exec_sessions 
    where session_id = @@SPID", conn);

            using (var reader = cmd.ExecuteReader())
            {
                while (reader.Read())
                {
                    Console.WriteLine("Isolation Level = " + reader.GetValue(0) + ", SPID = " + reader.GetValue(1));
                }
            }

            if (tran != null) tran.Complete();
        }
    }
}

出力:

Isolation Level = ReadCommitted, SPID = 51
Isolation Level = Serializable, SPID = 51
Isolation Level = Serializable, SPID = 51 //leaked!
55
usr

SQL Server 2014では、これは修正されたようです。 TDSプロトコル7. 以上を使用している場合。

SQL Serverバージョン12.0.2000.8で実行すると、出力は次のようになります。

ReadCommitted
Serializable
ReadCommitted

残念ながら、この変更は次のようなドキュメントには記載されていません。

ただし、変更はMicrosoftフォーラムで文書化されています。

2017年3月8日更新

残念ながら、これはSQL Server 2014 CU6およびSQL Server 2014 SP1 CU1でバグが発生したため、後で「未修正」になりました。

FIX:SQL Server 2014でSQL Server接続が解放されると、トランザクション分離レベルが誤ってリセットされます

「SQL Serverクライアント側のソースコードでTransactionScopeクラスを使用し、トランザクションでSQL Server接続を明示的に開かないとします。SQLServer接続が解放されると、トランザクション分離レベルが誤ってリセットされます。」

18
Thomas

接続プールは、接続をリサイクルする前にsp_resetconnectionを呼び出します。トランザクション分離レベルのリセットは 物事のリストにはありません で、sp_resetconnectionが行います。これが、プールされた接続全体で「シリアライズ可能」なリークが発生する理由を説明しています。

正しい分離レベル であることを確認することで、各クエリを開始できると思います。

_if not exists (
              select  * 
              from    sys.dm_exec_sessions 
              where   session_id = @@SPID 
                      and transaction_isolation_level = 2
              )
    set transaction isolation level read committed
_

別のオプション:異なる接続文字列を持つ接続は、接続プールを共有しません。そのため、「シリアライズ可能」クエリに別の接続文字列を使用すると、「読み取りコミット」クエリとプールを共有しません。接続文字列を変更する簡単な方法は、別のログインを使用することです。 _Persist Security Info=False;_のようなランダムオプションを追加することもできます。

最後に、すべての「シリアライズ可能」クエリが戻る前に分離レベルをリセットすることを確認できます。 「シリアライズ可能」クエリが完了しない場合、汚染された接続を強制的にプールから外す 接続プールをクリア できます。

_SqlConnection.ClearPool(yourSqlConnection);
_

これはコストがかかる可能性がありますが、クエリが失敗することはまれであるため、ClearPool()を頻繁に呼び出す必要はありません。

29
Andomar

私はこのトピックについて質問し、C#コードを追加しました。これは、この問題を回避するのに役立ちます(つまり、1つのトランザクションの分離レベルのみを変更します)。

個々のADO.NETトランザクションでのみ分離レベルを変更

これは基本的に、「using」ブロックでラップされるクラスであり、前に元の分離レベルを照会し、後で復元します。

ただし、デフォルトの分離レベルを確認して復元するには、DBへの2つの追加のラウンドトリップが必要です。変更された分離レベルがリークしないことは確実ではありませんが、その危険性はほとんどありません。

0
Erik Hart