コンピューターサイエンス:アルゴリズム / OS / ネットワーク / DB

デッドロックと回避

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

デッドロックと回避

このレッスンで分かること

  • デッドロックが発生する条件
  • DBMS がデッドロックを検出する仕組み
  • アプリ側で予防する 4 つの設計原則

デッドロックと回避 とは

デッドロックが起きる原理と運用上の対策。本レッスンでは、デッドロックと回避 の基本から実際の使いどころまでを整理し、現場で迷わず使える形に落とし込みます。

デッドロックとは

デッドロック (Deadlock) とは、複数のトランザクションが 互いに相手のロックを待ち続ける 状態です。永遠に解消されないため、どちらかを強制終了させる必要があります。

最小の例は次の通りです。

プレーンテキスト

T1: UPDATE accounts SET balance=balance-100 WHERE id=1; -- 行 1 をロック T2: UPDATE accounts SET balance=balance-200 WHERE id=2; -- 行 2 をロック T1: UPDATE accounts SET balance=balance+100 WHERE id=2; -- 行 2 待ち T2: UPDATE accounts SET balance=balance+200 WHERE id=1; -- 行 1 待ち

T1 は行 2 のロックを T2 から待ち、T2 は行 1 のロックを T1 から待つ。誰も進めません。

diagram (will load when visible)

DBMS の検出

DBMS は 待機グラフ (Wait-For Graph) を内部に持ち、定期的にサイクルを検出します。サイクルが見つかれば、それがデッドロックです。

検出するとどちらかのトランザクションを 被害者 (victim) として選び、強制的に ROLLBACK させます。選択基準は「巻き戻しのコストが小さい方」が一般的で、変更量の少ない方が犠牲になります。

プレーンテキスト

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

MySQL なら SHOW ENGINE INNODB STATUS で直近のデッドロック詳細を確認できます。

デッドロックを防ぐ 4 原則

原則 1:ロック取得順を揃える

すべてのトランザクションが 同じ順序 でロックを取れば、サイクルは発生しません。

プレーンテキスト

× T1: 行 1 → 行 2 T2: 行 2 → 行 1 ← 逆順がデッドロックの種 ○ T1: 行 1 → 行 2 T2: 行 1 → 行 2 ← 順序を揃える

実装上は「ID の小さい順に SELECT FOR UPDATE」のようなルールにします。

原則 2:トランザクションを短くする

ロック保持時間が短いほど、衝突確率が下がります。具体的には次のものを徹底します。

  • 外部 API 呼び出しをトランザクション外に出す
  • 重い計算をトランザクション外で済ます
  • 入力待ちのユーザー操作をトランザクション内に入れない

原則 3:適切なインデックスを貼る

InnoDB の行ロックは インデックスを通じて 取得されます。インデックスがないと テーブル全体をロック することになり、デッドロックが頻発します。

SQL クエリ

UPDATE orders SET status = 'shipped' WHERE order_no = 'A100';

order_no にインデックスがないと、全行スキャンしながら全行ロックになる可能性があります。WHERE の列にはインデックスを貼っておきましょう。

原則 4:再試行ロジックを入れる

デッドロックは 発生を完全には防げません。発生時にアプリ側で 自動再試行 する仕組みが必須です。

JavaScript

async function executeWithRetry(operation, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { return await operation(); } catch (err) { if (err.code === 'ER_LOCK_DEADLOCK' && i < maxRetries - 1) { await sleep(Math.random() * 100 * (i + 1)); // jitter 付き backoff continue; } throw err; } } }

ランダムな待ち時間(ジッター)を入れて、再試行同士の衝突を避けます。

ロックの種類と粒度

InnoDB のロックは複数の種類があります。

  • Record Lock — 単一行のロック
  • Gap Lock — 行と行の間の隙間のロック(ファントム防止)
  • Next-key Lock — Record + Gap の組み合わせ
  • Insert Intention Lock — 挿入時に取る軽量ロック

Repeatable Read 分離レベルでは Next-key Lock が多用され、これがデッドロックの主因になることもあります。Read Committed に下げると Gap Lock がなくなり、デッドロックが減ることがあります。

デッドロックが減らないときは、分離レベルを 1 段下げて再現するか確認するのも手。

ライブロックという別問題

「両方が譲り合って永遠に進まない」ライブロック という現象もあります。デッドロックと違い、ロック自体は持っていないので DBMS は検出できません。指数バックオフとジッターで対処します。

実例の現場テクニック

  • ORDER BY id FOR UPDATE — 範囲ロックを ID 順で取り、順序を揃える
  • SKIP LOCKED — ロック済みの行を飛ばす(タスクキューで便利)
  • アプリケーションロック — Redis の SETNX で外部ロックを取り、DB の行ロックを減らす

やってみよう

  • 2 セッションで上記のデッドロックを実際に再現してみる
  • MySQL の SHOW ENGINE INNODB STATUSLATEST DETECTED DEADLOCK を読む
  • アプリに再試行ロジックを入れ、わざとデッドロックを起こして自動回復するか確認

よくある質問

Q. デッドロックを未然に防ぐには?

A. ロック取得順を全コードで統一する、トランザクションを短くする、ロック数を最小化する、タイムアウトを設定するなどが定石です。DB なら SELECT FOR UPDATE の順番を主キー昇順にそろえると衝突確率が大きく下がります。

Q. デッドロックが起きたら DB はどうしますか?

A. MySQL/PostgreSQL は片方のトランザクションを自動でロールバックして解消します。アプリは ERROR 1213 / SerializationFailure を catch して、指数バックオフで再試行する実装が定石です。再試行回数の上限を必ず設けてください。

Q. ロック粒度はどうやって決めますか?

A. 粗いロック(テーブル全体)は競合が増え、細かいロック(行レベル)は管理コストが増えます。OLTP は行ロック、バッチ処理はテーブルロックという使い分けが一般的です。負荷テストで競合率を計測しながら段階的に細かくしていくのが現実的です

次のレッスン

次は クエリプランナの役割 で、デッドロックが起きる原理と運用上の対策 を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. デッドロックと回避 の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. デッドロックと回避 とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン