[Laravel] 他システムとのデッドロック発生時の対応メモ
自システムvs他システムのデッドロック対応メモ
概要
他システム→自システムへの更新で、特定テーブルのデッドロックが発生しました。
突然発生し始めたように見えて、発生直後は焦りました。
直接原因
自システムでモデルが作成されたのち、以下の状態になったためデッドロックが起きてしまいました。
TRX1:モデルA作成 → モデルイベントでモデルB作成 → モデルイベントでモデルAを更新
TRX2:モデルAを他システムへ連携 → 他システムで一意なIDを発行 → モデルAを更新
同期処理と非同期処理が被ってしまったパターンです。
復旧作業
データリペア
- 自システムと他システムのデータ状態確認
- データリペア方法の確立
- データリペア
原因調査
- SHOW ENGINE INNODB STATUS でデッドロック状況確認
- データパターンによる傾向分析
- 発生開始日時前後のリリース・システム変更確認
- 発生頻度確認
暫定対応 : リトライ処理の追加
実装内容
他システムで発行された一意のIDをモデルAに保存する際の処理で、try-catchを入れて複数回リトライする実装を行なっていましたが、他の方がcopilotに聞いたところ、なんとLaravelにはデッドロック発生時にリトライできる仕組みがあるようでした!
上記ドキュメントによると、初回リクエストも1回とカウントされるため、"2" で設定しました。
変更内容:
// 変更前
DB::transaction(function () {
$this->cooperation_package->saveModel();
});
// 変更後
DB::transaction(function () {
$this->cooperation_package->saveModel();
}, 2); // ← リトライ回数を2に設定(初回 + 1回リトライ)
リトライで成功する保証は無い、かつ、根本解決ではないですが、状況的に絶妙なタイミングで発生していそうな雰囲気だったため、本対応で一旦障害は落ち着きました。良かった。。
根本解決
恒久対応 : 排他ロックによる根本解決
実装内容
既存の処理を追ったところ、自システムではSロック(共有ロック)を取得してからXロック(排他ロック)を取得していましたが、この間に他システムからXロックを要求されたことでデッドロックとなっていました。
データの新規作成時であり、データを参照される可能性が低いことから、自システムでも最初からXロックを取得する方針で考えました。
変更内容:
// 他システムからの更新処理と競合を防ぐため、事前にモデルAのXロックを取得
$model_a = ModelA::lockForUpdate()->findOrFail($model_b->modelA_id);
この改修を入れることでデッドロック自体、発生しなくなりました。
効果:
- モデルB作成時に、モデルAのレコードをロック
- 他システムの更新処理は、ロックが解放されるまで待機
- 順序が保証され、デッドロックが発生しない
まとめ
暫定対応
障害が頻発していたため即座にデプロイ可能なリトライ処理を追加することで、一時的にエラーを抑制
恒久対応
FOR UPDATE で排他ロックを取得することで、他システムとの競合を回避
おわりに
デッドロックの対応をするまで、DB:transactionでリトライ出来ることも知らなかったですし、恥ずかしながら FOR UPDATE を使ったこともありませんでした。。
怪我の功名というか、現場経験積むのが一番成長に繋がることを再認識できました。
また、これは発生日時のリリース状況からの推測ですが、元々長いモデルイベントに処理を追加したことでロックの時間が長くなり、デッドロックに繋がったと考えています。
改めてモデルイベントへの抵抗感が強くなった気もします。。
Discussion