デッドロックガイド
1.デッドロックを理解する
デッドロックは、通常は複数ステップのトランザクション内で、データベース内で同時実行ロックが競合した結果です。
デッドロックは、2 つ以上のタスクが相互に永続的にブロックし、他のタスクがロックしようとしているリソースをそれぞれがロックすると発生します。例えば:
- トランザクション A は、行 1 で共有ロックを取得します。
- トランザクション B は、行 2 で共有ロックを取得します。
- トランザクション A は行 2 の排他ロックを要求し、トランザクション B が完了して行 2 の共有ロックを解放するまでブロックされます。
- トランザクション B は行 1 の排他ロックを要求し、トランザクション A が完了して行 1 の共有ロックを解放するまでブロックされます。
トランザクション A はトランザクション B が完了するまで完了できませんが、トランザクション B はトランザクション A によってブロックされます。この状態は、循環依存関係とも呼ばれます。トランザクション A はトランザクション B に依存し、トランザクション B はトランザクション A への依存関係を通じてサイクルを閉じます。
デッドロック状態の 2 つのトランザクションは、外部プロセスによってデッドロックが解除されない限り、永遠に待機します。SQL Server データベース エンジン デッドロック モニターは、デッドロック状態のタスクを定期的にチェックします。モニターが循環依存を検出すると、タスクの 1 つを犠牲として選択し、エラーが発生した場合はそのトランザクションを終了します。これにより、他のタスクがトランザクションを完了することができます。エラーで終了するトランザクションを持つアプリケーションは、トランザクションを再試行できます。これは通常、別のデッドロックされたトランザクションが完了した後に完了します。
デッドロックは、通常のブロッキングと混同されることがよくあります。トランザクションが別のトランザクションによってロックされているリソースのロックを要求すると、要求元のトランザクションはロックが解放されるまで待機します。既定では、LOCK_TIMEOUT が設定されていない限り、SQL Server トランザクションはタイムアウトしません。要求側のトランザクションは、ロックを所有するトランザクションをブロックするために何もしなかったため、デッドロックではなくブロックされます。最終的に、所有しているトランザクションが完了してロックが解放され、要求しているトランザクションにロックが付与されて続行されます。デッドロックはほぼ瞬時に解決できますが、ブロッキングは理論的には無期限に持続する可能性があります。デッドロックは致命的な抱擁と呼ばれることもあります。
デッドロックは、リレーショナル データベース管理システムだけでなく、複数のスレッドを持つ任意のシステムで発生する可能性がある状態であり、データベース オブジェクトのロック以外のリソースで発生する可能性があります。たとえば、マルチスレッド オペレーティング システムのスレッドは、メモリ ブロックなどの 1 つ以上のリソースを取得する場合があります。取得中のリソースが現在別のスレッドによって所有されている場合、最初のスレッドは、所有しているスレッドがターゲット リソースを解放するまで待機する必要がある場合があります。待機中のスレッドは、その特定のリソースを所有しているスレッドに依存していると言われています。SQL Server データベース エンジンのインスタンスでは、メモリやスレッドなどのデータベース以外のリソースを取得しているときに、セッションがデッドロックする可能性があります。
この図では、トランザクション T1 は、テーブル ロック リソースのトランザクション T2 に依存しています。同様に、トランザクション T2 は、テーブル ロック リソースについてトランザクション T1 に依存しています。これらの依存関係が循環するため、トランザクション T1 と T2 の間にデッドロックが発生します。
AUTO に設定された設定でテーブルが分割されている場合にも、デッドロックが発生する可能性があります。AUTO に設定すると、SQL Server データベース エンジンがテーブル レベルではなく HoBT レベルでテーブル パーティションをロックできるようになるため、同時実行性が向上します。ただし、別のトランザクションがテーブルのパーティション ロックを保持していて、そのロックを別のトランザクションのパーティションのどこかで使用しようとすると、デッドロックが発生する可能性があります。このタイプのデッドロックは、 に設定することで防ぐことができますが、この設定を行うと、パーティションへの多数の更新が強制的にテーブル ロックを待機することになり、同時実行性が低下します。
2. デッドロックを検出して終了する
デッドロックは、2 つ以上のタスクが相互に永続的にブロックし、他のタスクがロックしようとしているリソースをそれぞれがロックすると発生します。次の図は、デッドロック ステータスの概要を示しています。
- タスク T1 は、リソース R1 (R1 から T1 への矢印で示される) のロックを保持しており、リソース R2 のロックを要求します (T1 から R2 への矢印で示されます)。
- タスク T2 はリソース R2 をロックしており (R2 から T2 への矢印で示されています)、リソース R1 のロックを要求しています (T2 から R1 への矢印で示されています)。
- リソースが使用可能になるまでどちらのタスクも続行できず、タスクが続行されるまでどちらのリソースも解放できないため、デッドロック状態が発生します。
SQL Server データベース エンジンは、SQL Server のデッドロック サイクルを自動的に検出します。SQL Server データベース エンジンはセッションの 1 つをデッドロックの対象として選択し、現在のトランザクションはデッドロックを解除するエラーで終了します。
2.1. デッドロックの可能性があるリソース
各ユーザー セッションは、その代わりに 1 つ以上のタスクを実行できます。各タスクは、さまざまなリソースを取得または取得するために待機します。次のタイプのリソースは、ブロックを引き起こし、デッドロックを引き起こす可能性があります。
-
ロック。オブジェクト、ページ、行、メタデータ、アプリケーションなどのリソースに対するロックの取得を待機すると、デッドロックが発生する可能性があります。たとえば、トランザクション T1 は行 r1 に共有 (S) ロックを持っており、r2 の排他 (X) ロックの取得を待機しています。トランザクション T2 は、r2 で共有 (S) ロックを保持しており、行 r1 で排他 (X) ロックの取得を待機しています。これにより、ロック ループが発生し、T1 と T2 が互いにロックされたリソースを解放するのを待ちます。
-
ワーカー スレッド。使用可能なワーカー スレッドを待機しているキューに入れられたタスクは、デッドロックを引き起こす可能性があります。キューに入れられたタスクが、すべてのワーカー スレッドをブロックするリソースを所有している場合、デッドロックが発生します。たとえば、セッション S1 がトランザクションを開始し、行 r1 で共有 (S) ロックを取得した後、スリープ状態になります。使用可能なすべてのワーカー スレッドで実行されているアクティブなセッションが、行 r1 の排他 (X) ロックを取得しようとしています。セッション S1 はワーカー スレッドを取得できないため、トランザクションをコミットして行 r1 のロックを解放することはできません。これにより、デッドロックが発生します。
-
メモリー。デッドロックは、使用可能なメモリでは満たすことができないメモリ許可を同時要求が待機している場合に発生する可能性があります。たとえば、ユーザー定義関数として実行される 2 つの同時クエリ Q1 と Q2 は、それぞれ 10 MB と 20 MB のメモリをフェッチします。各クエリに 30 MB が必要で、使用可能なメモリの合計が 20 MB の場合、Q1 と Q2 は互いにメモリが解放されるのを待つ必要があり、デッドロックが発生します。
-
クエリの並列実行に関連するリソース。スイッチ ポートに関連付けられたコーディネーター、プロデューサー、またはコンシューマー スレッドは、通常、並列クエリの一部ではない他のプロセスが少なくとも 1 つ関与している場合に、互いにブロックしてデッドロックを引き起こす可能性があります。また、並列クエリの実行が開始されると、SQL Server は現在のワークロードに基づいて、並列処理の度合い、つまりワーカー スレッドの数を決定します。システムのワークロードが予期せず変化すると、デッドロックが発生する可能性があります (たとえば、サーバーで新しいクエリの実行が開始されたり、システムでワーカー スレッドが不足したりするなど)。
-
複数のアクティブな結果セット (MARS) リソース。ユーザー リソース、セッション ミューテックス、トランザクション ミューテックスなどのこれらのリソースは、MARS で複数のアクティブな要求のインターリーブを制御するために使用されます。
タスクを MARS で実行するには、セッション ミューテックスを取得する必要があります。タスクがトランザクションで実行されている場合、トランザクション ミューテックスを取得する必要があります。これにより、特定のセッションおよび特定のトランザクションで、一度に 1 つのタスクのみがアクティブになることが保証されます。必要なミューテックスを取得したら、タスクを実行できます。タスクがリクエストの途中で完了または譲歩すると、まずトランザクション ミューテックスを解放し、次にセッション ミューテックスを取得の逆順で解放します。ただし、これらのリソースはデッドロックする可能性があります。次の疑似コードでは、2 つのタスク (ユーザー要求 U1 とユーザー要求 U2) が同じセッションで実行されます。
ユーザー要求 U1 から実行されたストアド プロシージャは、セッション ミューテックスを取得しています。ストアド プロシージャの実行に時間がかかる場合、SQL Server データベース エンジンは、ストアド プロシージャがユーザー入力を待機していると見なします。ユーザー要求 U2 はセッション ミューテックスを待機していますが、ユーザーは U2 からの結果セットを待機しており、U1 はユーザー リソースを待機しています。これはデッドロック状態であり、論理的に次のように説明されます。
3. デッドロックへの対処
SQL Server データベース エンジン のインスタンスがトランザクションをデッドロックの対象として選択すると、現在のバッチを終了し、トランザクションをロールバックして、エラー メッセージ 1205 をアプリケーションに返します。
Transact-SQL クエリを送信する任意のアプリケーションがデッドロックの犠牲者として選択される可能性があるため、アプリケーションには、エラー メッセージ 1205 をキャッチできるエラー ハンドラーが必要です。アプリケーションがエラーをキャッチしない場合、アプリケーションは、トランザクションがロールバックされ、エラーが発生した可能性があることを知らずに続行できます。
エラー メッセージ 1205 をキャッチするエラー ハンドラーを実装すると、アプリケーションはデッドロックの状況を処理し、是正措置を取ることができます (たとえば、デッドロックに関係するクエリを自動的に再送信します)。クエリを自動的に再送信することにより、ユーザーはデッドロックが発生したことを知る必要がありません。
クエリを再送信する前に、アプリケーションを一時停止する必要があります。これにより、デッドロックに関係する他のトランザクションが、デッドロック サイクルの一部を形成したロックを完了して解放する機会が与えられます。これにより、再送信されたクエリがそのロックを要求したときにデッドロックが再発する可能性が最小限に抑えられます。
4.デッドロックを最小限に抑える
デッドロックを完全に回避することはできませんが、特定のコーディング規則に従うことで、デッドロックが発生する可能性を最小限に抑えることができます。デッドロックを最小限に抑えると、トランザクションが少なくなるため、トランザクションのスループットが向上し、システムのオーバーヘッドが削減されます。
- トランザクションによって実行されたすべての作業を元に戻すロールバック。
- デッドロック時にロールバックされたため、アプリケーションによって再送信されました。
デッドロックを最小限に抑えるには、次のようにします。
- オブジェクトは同じ順序でアクセスされます。
- トランザクションでのユーザーの操作を避け、トランザクションを短くバッチ処理します。
- 低い分離レベルを使用してください。
- 行のバージョン管理に基づく分離レベルを使用します。データベース オプションを設定して、読み取りコミット トランザクションで行のバージョン管理を使用できるようにし、スナップショット分離を使用します。
- バインド接続を使用します。
4.1. 同じ順序でオブジェクトにアクセスする
すべての同時トランザクションが同じ順序でオブジェクトにアクセスすると、デッドロックが発生する可能性が低くなります。たとえば、2 つの同時トランザクションがテーブルのロックを取得し、次にテーブルのロックを取得した場合、一方のトランザクションは、もう一方のトランザクションが完了するまでテーブルでブロックされます。最初のトランザクションがコミットまたはロールバックされた後、2 番目のトランザクションはデッドロックなしで続行されます。すべてのデータ変更にストアド プロシージャを使用すると、オブジェクトにアクセスする順序が標準化されます。
4.2. トランザクションでのユーザー操作を避ける
ユーザーの介入なしで実行できるバッチは、アプリケーション要求のパラメーター プロンプトへの応答など、手動でクエリに応答する必要があるユーザーよりもはるかに高速であるため、ユーザーの操作を含むトランザクションの作成は避けてください。たとえば、トランザクションがユーザーの入力を待っているときに、ユーザーが週末にランチに行ったり、家に帰ったりした場合、ユーザーはトランザクションの完了を遅らせます。トランザクションによって保持されているロックは、トランザクションがコミットまたはロールバックされたときにのみ解放されるため、これによりシステムのスループットが低下します。デッドロック状態が発生していない場合でも、同じリソースにアクセスする他のトランザクションは、トランザクションの完了を待機している間ブロックされます。
4.3. トランザクションを短くし、1 つのバッチにまとめる
通常、デッドロックは、複数の実行時間の長いトランザクションが同じデータベースで同時に実行されている場合に発生します。トランザクションが長くなるほど、排他ロックまたは更新ロックが保持される時間が長くなり、他のアクティビティが妨げられ、デッドロック状態になる可能性があります。
トランザクションをバッチで保存すると、トランザクション中のネットワーク ラウンドトリップが最小限に抑えられ、トランザクションの完了とロックの解放で発生する可能性のある遅延が減少します。
4.4. 低い分離レベルを使用する
低い分離レベルでトランザクションを実行できるかどうかを決定します。読み取りコミットを実装すると、トランザクションは、最初のトランザクションが完了するのを待たずに、別のトランザクションによって以前に読み取られた (変更されていない) データを読み取ることができます。共有ロックは、高い分離レベル (Serializable など) よりも低い分離レベル (Read Committed など) で保持される期間が短くなります。これにより、ロックの競合が減少します。
4.5. 行のバージョン管理に基づく分離レベルを使用する
データベース オプションが ON に設定されている場合、読み取りコミット分離レベルで実行されているトランザクションは、読み取り操作中に共有ロックの代わりに行のバージョン管理を使用します。
スナップショット分離も行のバージョン管理を使用し、読み取り操作中に共有ロックを使用しません。スナップショット分離でトランザクションを実行するには、データベース オプション ALLOW_SNAPSHOT_ISOLATIONON を設定する必要があります。
これらの分離レベルは、読み取り操作と書き込み操作の間に発生する可能性のあるデッドロックを最小限に抑えるために実装されています。
4.6. バインド接続を使用する
バインドされた接続を使用すると、同じアプリケーションによって開かれた 2 つ以上の接続が相互に連携できます。セカンダリ接続によって取得されたロックは、プライマリ接続によって取得されたかのように保持され、その逆も同様です。したがって、それらは互いにブロックしません。
4.7. トランザクションを停止する
デッドロックのシナリオでは、被害者のトランザクションは自動的に停止され、ロールバックされます。デッドロック状態でトランザクションを停止する必要はありません。
要約する
一部のアプリケーションは、read-committed 分離のロックおよびブロック動作に依存しています。これらのアプリケーションでは、行のバージョン管理に基づく分離レベルの使用を有効にする前に、いくつかの変更が必要です。