「あ、詰んだ…」とならないために。PostgreSQLのデッドロックと上手に付き合う方法
現場でデータベースを触っていると、一度は必ず遭遇する「謎の停止」。特に高負荷なシステムや、複数のテーブルを複雑に更新するバッチ処理なんかを書いているとき、突然アプリケーションから「Deadlock detected」なんてエラーが返ってきて、冷や汗をかいた経験はありませんか?
今日は、そんな厄介者だけど避けては通れない「デッドロック」について、PostgreSQLが裏でどう動いているのか、そして僕たちがどう向き合うべきかを、実戦的な視点で深掘りしてみようと思います。
デッドロックって、結局なにが起きてるの?
教科書的に言うと「複数のトランザクションが、互いに相手が必要としているリソース(行ロックなど)を掴んで離さない状態」ですね。でも、これだとちょっとイメージしにくい。
わかりやすく例えるなら、「二人で狭い通路ですれ違うとき、お互いに相手が避けてくれるのを待って、一歩も動けなくなっている」状態です。
具体的な「詰み」のシナリオ
例えば、こんなケース。よくある「銀行口座の送金」を想像してください。
1. トランザクションA:口座1の残高を更新し、次に口座2を更新しようとする。
2. トランザクションB:口座2の残高を更新し、次に口座1を更新しようとする。
もしAが「口座1」をロックした瞬間に、Bが「口座2」をロックしたらどうなるか。
AはBが握っている「口座2」を待ち、BはAが握っている「口座1」を待つ。…はい、ここで永遠の待ちぼうけ(デッドロック)の完成です。
PostgreSQLはどうやって「救出」してくれるのか?
PostgreSQLは非常に賢いので、この状態を放置しません。内部で「デッドロック検知器(Deadlock Detector)」という監視役が常に目を光らせています。
もし、あるトランザクションがロック待ちに入ると、検知器は「この待ち状態はループになっていないか?」をチェックします。もしループを見つけたら、「よし、片方を犠牲にして道を空けよう」と判断し、どちらかのトランザクションを強制的にアボート(ロールバック)させます。
これが、皆さんが画面で見る「`ERROR: deadlock detected`」の正体です。PostgreSQLが自らを救うために、犠牲者を出しているわけですね。
実践:デッドロックを避けるための「鉄則」
現場でデッドロックを完全にゼロにするのは至難の業ですが、発生確率を劇的に下げることは可能です。僕が後輩によく伝えているのは、以下の3つのポイントです。
1. 更新順序を統一する
これが一番の特効薬です。さっきの送金例なら、「常に口座番号が小さい順からロックする」というルールをアプリ側で徹底するだけ。
- AもBも「口座1 → 口座2」の順で更新するようにすれば、Bは口座1をロックしようとした時点で待機状態になるため、デッドロックは起きません。
2. トランザクションをできるだけ短く保つ
ロックを握っている時間が長ければ長いほど、デッドロックの可能性は高まります。
- 「トランザクションの中で、外部APIを叩く」とか「重い計算処理をする」のはNG。
- ロックが必要な更新処理は、トランザクションの最後にまとめて持ってくるのが正解です。
3. ロック待ちのタイムアウトを適切に設定する
PostgreSQLには `deadlock_timeout` という設定値があります(デフォルトは1秒)。
「ロックが解放されないな?」とPostgreSQLが判断してからデッドロックチェックを開始するまでの時間です。これが短すぎると過剰なチェック負荷がかかり、長すぎるとシステムがしばらくフリーズしてしまいます。通常はデフォルトのままで良いことが多いですが、高負荷なシステムではこの挙動を意識しておくことが大切です。
最後に:エラーが出ても焦らないで
もし本番環境でデッドロックが発生しても、過剰に怯える必要はありません。重要なのは、「なぜその順序でロックを掴んだのか」という再現手順を特定することです。
ログにはデッドロックが発生した際の情報(どのクエリがどのリソースで待機していたか)がしっかり残ります。それを見て、「あ、この処理の順序、逆だったな」とか「この処理、トランザクションの範囲が広すぎるな」と分析する。その繰り返しが、データベースエンジニアとしての腕を上げる一番の近道です。
データベースは、僕たちの意図を忠実に実行しようと頑張っています。デッドロックは、そんな彼らが「もうこれ以上は無理!誰か助けて!」と悲鳴を上げている状態。そう思えば、少しだけ愛着が湧いてきませんか?
それでは、また次回の記事でお会いしましょう!良いデータベースライフを。
コメント