MySQL (InnoDB) デッドロックの発生原因とSHOW ENGINE INNODB STATUSによる解析

MySQL Database Performance SQL
結論

SHOW ENGINE INNODB STATUS で競合箇所を特定し、アプリ側で Error 1213 の自動再試行を実装します。

-- MySQL公式仕様:最新のデッドロックログを出力して原因解析
SHOW ENGINE INNODB STATUS\G

MySQL (InnoDB) 公式仕様:デッドロック処理メカニズム

MySQL公式ドキュメント(dev.mysql.com/doc/refman/8.0/en/innodb-deadlock-detection.html)の規定通り、InnoDB ストレージエンジンは複数トランザクション間でロック依存関係の循環(デッドロック)を常時監視しています。

  1. 自動検出(Deadlock Detection): 循環待ちを検知すると、InnoDB は影響行数の少ないトランザクション(Victim)を即座に選定して強制ロールバック(Error 1213)させます
  2. タイムアウト退避 (innodb_lock_wait_timeout): 検知機能がOFFの場合は innodb_lock_wait_timeout(デフォルト50秒)経過後に Error 1205 でタイムアウト処理されます

実際に起こる障害:Error 1213 による決済・データ欠損障害

データベース側のデッドロック検出は正常な自己防衛機能ですが、アプリケーション側で Error 1213 をハンドリングしていない場合、ユーザーへ 500 エラーが突然返り、注文データや決済処理が途中で消失する障害 が発生します。

RDBMSではデッドロックの完全ゼロ化は不可能なため、アプリ側での自動再試行(Retry)設計が前提となります。


対策手順と実装

  1. SHOW ENGINE INNODB STATUS\G を実行し、LATEST DETECTED DEADLOCK セクションのロック保持SQLと要求SQLを確認する
  2. アプリケーション全体で、複数テーブルを更新する際のクエリ順番(例: 必ず users テーブル更新 → orders テーブル更新の順)を統一する
  3. アプリのDBアクセス層で 1213 (Deadlock) をキャッチし、指数バックオフで数回再試行するロジックを実装する
// ⭕ Node.js (TypeScript) でのデッドロック自動再試行実装パターン
async function executeWithRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (err: any) {
      // MySQL Error 1213: ER_LOCK_DEADLOCK
      if (err.errno === 1213 && attempt < maxRetries) {
        console.warn(`デッドロック検知。再試行中 (${attempt}/${maxRetries})...`);
        await new Promise(res => setTimeout(res, attempt * 100)); // リトライ
        continue;
      }
      throw err;
    }
  }
  throw new Error('再試行上限に達しました');
}