Day20 後半のゴール
「“デッドロックを怖がる”から“パターンとして扱う”に変える」
前半で「デッドロックとは何か」「典型例(逆順ロック)」まではイメージできました。
後半では、もう一歩踏み込んで「どう防ぐか」「起きたときどう扱うか」「どこを見れば原因に近づけるか」を、実務寄りの視点で整理していきます。
デッドロックが起きやすいパターンを知っておく
「“やりがちな設計”を先に知っておくと避けやすい」
デッドロックは、完全にランダムに起きるわけではありません。
パターンがあります。代表的なものを、イメージで押さえておきましょう。
パターン1:複数行をバラバラの順番で更新する
前半でやった「口座1→2」「口座2→1」のように、
同じテーブルの複数行を、トランザクションごとに違う順番で UPDATE するパターンです。
これを避けるための基本ルールはシンプルです。
「複数行を更新するときは、必ず同じ順番でロックを取りに行く」
IDの昇順・降順、ソートキーを1つ決めておき、
どの処理でもその順番で UPDATE するようにします。
パターン2:複数テーブルを違う順番で触る
もう少し現場っぽい例を出します。
トランザクションA:
orders → order_items の順に更新する。
トランザクションB:
order_items → orders の順に更新する。
これも、「テーブルレベルでの A→B」「B→A」になっているので、
タイミング次第でデッドロックになります。
ここでも同じです。
「複数テーブルを更新するときも、順番を揃える」
「必ず親テーブルから子テーブルへ」「必ず orders → order_items の順」など、
アプリ全体でルールを決めておくと、デッドロックの芽をかなり摘めます。
デッドロックとトランザクションの“粒度”
「1トランザクションに詰め込みすぎると、ロックが絡みやすくなる」
もう1つ、デッドロックを増やしがちな要因が「トランザクションが大きすぎる」ことです。
1つのトランザクションの中で、
複数テーブルを更新する
大量の行を更新する
その間にユーザー入力待ちや外部API待ちが入る
といったことをすると、ロックを長時間握り続けることになります。
その間に、別のトランザクションが別の順番でロックを取りに来ると、
デッドロックの確率が上がります。
ここでの設計のポイントはこうです。
「本当に同じトランザクションに入れる必要がある処理だけをまとめる」
「ユーザーの確認待ち」「外部APIのレスポンス待ち」など、
時間が読めない処理は、できるだけトランザクションの外に出す。
トランザクションを「短く・小さく」保つことは、
MVCC的にもロック的にも、かなり効く基本戦略です。
デッドロックが起きたときの“正しいリアクション”
「エラーを見て、静かにリトライする」
PostgreSQL がデッドロックを検出すると、
どちらか一方のトランザクションにエラーを返します。
アプリ側でやるべきことは、感情的ではなく、パターンとして決めておくことです。
基本方針
「デッドロック検出エラーが来たら、そのトランザクションをロールバックし、
一定回数までリトライする」
これだけです。
重要なのは、「デッドロックは“異常事態”ではあるが、“想定外”ではない」と扱うことです。
高負荷時や、複数の処理が同じテーブルを触る場面では、
どうしても一定確率で発生します。
だからこそ、
アプリケーションコードのどこでトランザクションを開始し、どこで終わるか
デッドロックエラーをどの層でキャッチし、どの単位でリトライするか
を、設計として決めておくことが大事です。
デッドロックの原因を追うときに見る場所
「エラーメッセージとサーバログから“関係者”を特定する」
「防ぐ」だけでなく、「起きてしまったデッドロックの原因を知りたい」場面もあります。
そのときに見るべきものを整理しておきます。
エラーメッセージ
アプリ側に返ってくるエラーには、
「どのプロセスがどのロックを待っていたか」のヒントが含まれています。
ERROR: deadlock detected
DETAIL: Process 12345 waits for ShareLock on transaction 67890; blocked by process 67890.
HINT: See server log for query details.
ここで「See server log」と言われている通り、
本気で原因を追うときはサーバログを見ることになります。
サーバログ
PostgreSQL の設定で log_lock_waits や deadlock_timeout を適切に設定しておくと、
デッドロック発生時に「どのクエリ同士がぶつかったか」がログに出ます。
そこから、
どのテーブル・どの行(またはインデックス)がロック対象だったか
どの順番でロックを取りに行っていたか
を読み解き、「ロック順序を揃える」「トランザクションを分割する」などの対策につなげます。
ここは運用寄りの話ですが、
「デッドロックが出たらログを見れば“関係者”が分かる」という感覚だけ持っておくと、
怖さがだいぶ減ります。
デッドロックとセキュリティ・堅牢性の関係
「“落ち方”をコントロールできるかどうか」
セキュリティ・堅牢性の観点から見ると、
デッドロックは「どう落ちるか」が重要です。
もしアプリ側がデッドロックエラーを想定していないと、
ユーザーに意味不明なエラーを返す
中途半端な状態で処理が止まり、再実行もできない
ログにも原因が残らない
といった「運用的に危険な状態」になります。
逆に、
デッドロックエラーを検出して、静かにリトライする
リトライ回数を超えたら、ユーザーに「混雑中です。時間をおいて再度お試しください」と返す
サーバログには、デッドロックの詳細が残るようにしておく
といった設計にしておけば、
デッドロックは「たまに起きるが、システム全体を壊さないイベント」にできます。
これは、「障害に強いシステム設計」の一部でもあります。
Day20 後半のまとめ
デッドロックは、「複数行・複数テーブルを“バラバラの順番”でロックしに行く」「1トランザクションに処理を詰め込みすぎてロックを長時間握る」といったパターンで起きやすく、逆に「ロック順序を揃える」「トランザクションを短く保つ」というシンプルなルールでかなり発生率を下げられる。
PostgreSQL はデッドロックを検出すると一方のトランザクションを deadlock detected で強制ロールバックするので、アプリ側はこのエラーを前提に「リトライ戦略」を持ち、必要に応じてサーバログから「どのクエリ同士がぶつかったか」を特定してロック順序や設計を見直す――デッドロックを“謎の事故”ではなく“パターンとして扱える現象”にしていくことが、Day20 後半の着地点になります。
