MySQL(InnoDB)でデッドロックが起きると、片方のトランザクションがエラー 1213 でロールバックされる。SQL Server と違い、情報の取り先も典型パターンも少し違う。まず「何と何が待ち合ったか」を InnoDB のステータスから読むところから始める。
先に結論
- 発生時のエラーは 1213(ER_LOCK_DEADLOCK)。アプリ側では例外として拾える形で返る
- 待ち合いの全容は
SHOW ENGINE INNODB STATUS の「LATEST DETECTED DEADLOCK」に残る - 定番原因は、複数テーブル / 複数行へのアクセス順序のバラつきと、ギャップロック
- デッドロックは「完全には無くせない」前提で、発生率を下げる+アプリ側リトライが現実解
情報の取り方
- 直近1件は SHOW ENGINE INNODB STATUS の LATEST DETECTED DEADLOCK 節で読める(上書きされるので、発生を検知したら早めに取る)
- 常時記録したい場合は
innodb_print_all_deadlocks を有効化し、エラーログに残す - 読むポイントは「どのトランザクションが・どのロックを保持し・何を待っていたか」の組み合わせ
- performance_schema の data_lock_waits 系は、待ち状態の確認に使える(バージョンにより差がある)
典型パターン
- 更新順序の逆転: 処理Aは テーブルX→Y、処理Bは Y→X の順に更新し、互いに相手の解放を待つ
- ギャップロック: 既定の分離レベル(REPEATABLE READ)では、範囲条件の検索・更新で隙間のロックも取り、想定外の行同士が干渉する
- 一意でない条件での更新: 絞り込みが甘い更新はロック範囲が広がり、衝突確率が上がる
- 長いトランザクション: 保持時間が長いほど他の処理と重なりやすい。外部 API 呼び出しをトランザクション内に含めるのは典型的な悪化要因
対策
- 複数リソースへのアクセス順序を、処理間で統一する(主キー順に処理する等)。これが一番効く
- トランザクションは短く。必要な行だけ・必要な間だけロックする
- 更新条件にはインデックスを効かせ、ロック範囲を狭める
- 大量行のバッチ更新は分割して、1トランザクションの対象を小さくする
- 分離レベルを READ COMMITTED に変えるとギャップロック起因は減るが、挙動全体への影響確認が前提。安易な変更はしない
アプリ側リトライの設計
- エラー 1213 を検知したら、トランザクション全体を最初からやり直す(途中から再開はできない)
- リトライは回数上限+少し待ってから(即時リトライの集中は再衝突を招く)
- 冪等でない処理をリトライ対象にしない。設計側で冪等に寄せる
- リトライで吸収した件数もログに残す。頻発は設計問題のサインで、握りつぶさない
ざっくりまとめ
- まず INNODB STATUS で待ち合いの組み合わせを確定する。推測で直さない
- 順序統一・短いトランザクション・狭いロック範囲が三本柱
- 残りはリトライで吸収する前提の設計にし、頻度を監視する
関連記事