はじめに
「以前は正常に動いていたバッチが、一部のデータを取り込めなくなった」という報告から始まったデッドロック調査の記録です。
デッドロックは再現が難しく、ログから原因を追うのが基本になります。この記事では、調査の手順と原因の特定、対処までの流れを整理します。
症状
システム連携用のバッチが、一部のデータを取り込めないという報告がありました。データが消えているわけではなく、処理がスキップされているような状態でした。
調査手順
1. サーバーサイドのログを確認する
まずアプリケーションのログを確認しました。INSERT 処理の前後でデッドロックエラーが記録されていました。
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException:
Deadlock found when trying to get lock; try restarting transaction
デッドロックが原因で INSERT がロールバックされ、データが取り込まれていないことが分かりました。
2. デッドロックの発生頻度を確認する
バッチのログをさかのぼって確認すると、デッドロックは最近になって急増していることが分かりました。以前は散発的だったものが、特定の時期から頻発するようになっていました。
→ 最近何か変わったはずという仮説が立てられます。
3. MySQL のデッドロックログを確認する
MySQL の SHOW ENGINE INNODB STATUS でデッドロックの詳細を確認します。
SHOW ENGINE INNODB STATUS\G
出力の LATEST DETECTED DEADLOCK セクションを見ると、どのトランザクションがどのテーブル・行のロックを待っていたかが分かります。
今回の出力から、次の2点が分かりました。
- 特定のテーブルへの INSERT でデッドロックが発生している
- 複数のトランザクションが同時に同じリソースのロックを取得しようとしている
4. ロックの原因となっているクエリを特定する
コードを調べると、あるバッチが該当テーブルに対して WHERE 句なしで UPDATE を発行していることを発見しました。
-- 問題のクエリ(更新対象を絞っていない)
UPDATE production_detail SET status = 0
WHERE 句がないため、更新対象の行が広範囲に及び、結果として大量の行ロックを取得していました。その状態で同時実行される他のバッチの INSERT や UPDATE がロック待ちになっていました。
5. ロック取得順序の不一致を確認する
さらに調べると、ロック取得順序が逆になっている箇所がありました。
- バッチA:テーブルX → テーブルY の順にロックを取得
- バッチB:テーブルY → テーブルX の順にロックを取得
2つのトランザクションが逆順にロックを取得すると、互いに相手のロックを待ち続けるデッドロックが発生します。
トランザクションA: テーブルX をロック → テーブルY をロック待ち
トランザクションB: テーブルY をロック → テーブルX をロック待ち
→ 互いに待ち続けてデッドロック
6. 問題のバッチを特定して切り分ける
問題が疑われるバッチを一時的に無効化したところ、デッドロックが発生しなくなりました。これにより、そのバッチが原因であると確定できました。
対処
当面の対処:問題バッチの無効化
該当バッチは、現時点で実際には使用されていなかったため、一時的に無効化してデッドロックを解消しました。
パフォーマンス改善:キャッシュの導入
調査の過程で、取り込みバッチが参照マスタに対して処理のたびに複数回クエリを発行していることも確認しました。
アプリケーション側に HashMap を使ったキャッシュを実装し、マスタへの読み取り回数を削減しました。
// キャッシュを使って参照マスタの重複クエリを防ぐ
Map<String, MasterData> masterCache = new HashMap<>();
// 初回だけDBから取得してキャッシュに入れる
MasterData master = masterCache.computeIfAbsent(key, k -> masterRepository.findByKey(k));
この対応と合わせて MySQL の innodb_buffer_pool_size とアプリケーションサーバーのヒープメモリも増強した結果、1000件あたりの処理時間が約6分から約1分に短縮しました。
根本対処の方針(再発防止)
今回は応急処置として無効化しましたが、根本的な対処として次を検討しています。
- WHERE 句なしの UPDATE を行ロック単位の UPDATE に修正する
- 複数テーブルをロックするトランザクションのロック取得順序を統一する
- バッチ処理に監視・アラートを追加して異常を早期検知する
デッドロック調査のチェックリスト
まとめると、デッドロック調査の手順は次のとおりです。
- アプリケーションログでデッドロックエラーの有無を確認する
- 発生頻度の変化を確認する(最近増えたか?いつから?)
-
SHOW ENGINE INNODB STATUSでデッドロックの詳細を確認する - 関係するテーブルへのクエリを洗い出し、WHERE 句なしのロック範囲が広いクエリを探す
- 複数テーブルをまたぐトランザクションのロック取得順序が一致しているかを確認する
- 原因と思われる処理を切り分けて無効化し、デッドロックが解消するかを確認する
デッドロックを起こしにくくするための設計
WHERE 句なしの UPDATE は避ける
更新対象が広すぎる UPDATE はデッドロックのリスクが高いです。InnoDB は基本的に行ロックで動きますが、広範囲の更新は大量の行をロックし、競合を起こしやすくなります。可能な限り WHERE 句で更新対象を絞ります。
-- 避けるべき(更新対象が広すぎる)
UPDATE production_detail SET status = 0
-- こうする(更新対象を絞る)
UPDATE production_detail SET status = 0 WHERE id = :id
複数テーブルをまたぐ場合はロック順序を統一する
複数のトランザクションが同じテーブルをロックする場合、取得順序を統一するとデッドロックが起きにくくなります。
設計レビュー時に、複数テーブルをまたぐ更新処理のロック順序を確認する習慣を持つと安全です。
まとめ
- デッドロックの調査は
SHOW ENGINE INNODB STATUSとアプリログの組み合わせで進める - WHERE 句なしや更新範囲が広すぎる UPDATE は、大量の行ロックを取得してデッドロックの原因になりやすい
- 複数テーブルをまたぐトランザクションはロック取得順序の不一致がデッドロックを引き起こす
- 原因バッチを一時的に無効化して切り分けることで原因を確定できる
- アプリ側キャッシュでマスタへの重複クエリを減らすとパフォーマンスも改善する