Query Go
デッドロックを避ける - デッドロック (Deadlock) の使い方・オプション・サンプル

デッドロックを避ける - デッドロック (Deadlock)

複数トランザクションが互いのロックを待ち続けて止まる状態。検知とアプリ側のリトライが基本

概念図

デッドロック (Deadlock) diagram

構文

sql
-- DB が自動検知し 1 方をエラー終了させる (ex: SQLSTATE 40P01)

サンプル

A と B が互いのロックを待ち合って膠着。DB が片方を中断してデッドロックを解消する

sql
-- セッション A
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- セッション B が id=2 を先に更新している
UPDATE accounts SET balance = balance + 100 WHERE id = 2;  -- 待機

-- セッション B
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 2;
UPDATE accounts SET balance = balance + 100 WHERE id = 1;  -- 待機
-- → デッドロック検知、B が ROLLBACK される

デッドロックが起きる 4 条件

OS の教科書でも有名な Coffman 条件がトランザクションにも当てはまります。

  1. 相互排他: 同じロックは 1 人しか持てない
  2. 占有と待機: ロックを持ったまま別のロックを待つ
  3. 非横取り: 持っているロックは強制的に奪えない
  4. 循環待ち: A→B→…→A と輪になる

1 つでも崩せばデッドロックは起きないので、多くの対策は「循環待ちを作らない」ことを狙います。

検知と自動ロールバック

主要な RDBMS はデッドロックを自動検知します。

  • PostgreSQL: deadlock_timeout (既定 1 秒) 経過後に検知、片方を ERROR: deadlock detected (SQLSTATE 40P01) で中断
  • MySQL (InnoDB): 待機グラフをリアルタイムに監視し即座に検知、ERROR 1213 で片方をロールバック
  • SQL Server: デッドロックモニターが定期的に検査し、コストの低い「犠牲者 (deadlock victim)」を選んで Error 1205 で終了
  • Oracle: 検知すると ORA-00060 を返し、片方の文 (トランザクション全体ではない) をロールバック

アプリ側は該当エラーコードを捕まえて、短い遅延を入れてトランザクション全体を再試行するのが定石です。

sql
-- 擬似コード: デッドロック時の指数バックオフ付きリトライ
for attempt in 1..3:
  try:
    BEGIN;
    do_work();
    COMMIT;
    break;
  except DeadlockError:
    ROLLBACK;
    sleep(2 ** attempt * 50ms);

予防策: ロック順序の統一

循環待ちを防ぐ最も効果的な方法は すべてのトランザクションで同じ順序で行をロックすることです。たとえば銀行振込で 2 口座を更新するなら、常に id の小さい方から先にロックします。

sql
-- 必ず小さい id から先に更新
BEGIN;
SELECT * FROM accounts WHERE id = LEAST(:a, :b) FOR UPDATE;
SELECT * FROM accounts WHERE id = GREATEST(:a, :b) FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = :a;
UPDATE accounts SET balance = balance + 100 WHERE id = :b;
COMMIT;

その他の予防策

  • トランザクションを短く保つ: ロックの保持時間が短いほど衝突確率が下がる
  • インデックスを適切に張る: フルテーブルスキャンで無関係な行にもロックがかかり、衝突面積が広がるのを避ける
  • バッチで行順を揃える: ORDER BY id で並べてから更新
  • 必要なら粒度の粗いロックを先に: 行ロックの前にテーブルロック相当のアドバイザリロック (pg_advisory_xact_lock) でシリアライズ
  • リトライを前提に設計: どれだけ予防しても 0 にはならない。リトライは必ず実装

リトライ不能なケースに注意

デッドロックで中断された後、単純にトランザクションをもう一度流せば良いとは限りません。

  • トランザクション中で外部 API を呼んでいる場合、リトライで二重実行になる
  • アプリ側のインメモリ状態が中途半端に進んでいると再実行時の入力が違う
  • Oracle のように「文だけ」をロールバックするケースでは、アプリが気付かないまま続行してしまう可能性がある

外部副作用はトランザクション外へ、アプリ状態は DB の値から再構築、エラーコードは明示的にチェック、これを徹底してください。

関連トピック