「デッドロック」で詰んだ時の話。PostgreSQLはどうやってこの迷宮を解決しているのか?
やあ。データベースのパフォーマンスチューニングに追われる毎日、お疲れ様。
今日は現場で一番「心臓が止まりそうになる」瞬間の一つ、デッドロックについて話をしよう。
「あ、またデッドロック検知でトランザクションがロールバックされた……」
運用中、ログにこんなメッセージが流れてきて青ざめた経験、一度はあるよね?
でも、PostgreSQLがどうやってこの複雑な「膠着状態」を見抜き、どうやって裁きを下しているのか、その裏側のロジックまで深く理解しているエンジニアは意外と少ない。今日は、PostgreSQLの「デッドロック検出」という名の名探偵が、裏でどんな仕事をしているのか、少し深掘りしてみよう。
—
1. そもそも、なぜ「デッドロック」は起きるのか?
簡単に言うと、「お互いが相手の持っているリソースを欲しがり、永遠に離さない状態」だよね。
- トランザクションA:テーブルXをロック中 → テーブルYをロックしたい
- トランザクションB:テーブルYをロック中 → テーブルXをロックしたい
こうなると、どちらも相手がロックを解除するのを待つことになる。これが「待ちのループ」だ。PostgreSQLは放っておくとこれを永遠に待ってしまうから、どこかで誰かが「お前ら、もう無理だろ!」と止めてやる必要がある。
2. PostgreSQLの「デッドロック検出」の仕組み
PostgreSQLには、専用の「DeadlockDetector」というプロセスが常駐しているわけじゃない。実は、待たされているトランザクション自身が、定期的に「自分がデッドロックに巻き込まれていないか」を調査するという仕組みになっているんだ。
待機グラフ(Wait-for Graph)の構築
PostgreSQLは、ロック待ちが発生したとき、メモリ上に「誰が誰を待っているか」という関係図(待機グラフ)を作成する。
- 頂点(Vertex):トランザクション
- 辺(Edge):待ち関係(T1がT2のロックを待っている)
もしこのグラフの中に「循環(サイクル)」が見つかったら? そう、それがデッドロックの正体だ。
検出プロセス:即時検知 vs タイムアウト
実はPostgreSQLの面白いところは、即座に「お、循環したな!」と判断するわけではないことだ。
1. ロック待ち発生: トランザクションはロックを獲得できないと `deadlock_timeout`(デフォルトは1秒)だけ待つ。
2. 調査開始: このタイムアウト時間を過ぎてもロックが取れないと、ようやく「本当にデッドロックなのか?」を疑い始める。
3. グラフ探索: そこで初めて待機グラフをスキャンし、循環がないかをチェックする。
4. 裁き: 循環が見つかれば、待たされていたトランザクションの片方を強制的にロールバックさせる。
「1秒待つ」という仕様は、一見非効率に見えるかもしれない。でも、頻繁に高負荷なグラフ探索を行わずに済むという、パフォーマンスとの絶妙なトレードオフなんだよ。
—
3. 実務でのトラブルシューティング:コードで再現してみる
理屈はわかった。じゃあ、実務でデッドロックに遭遇したとき、どうやって解析すればいいのか?
まずはあえてデッドロックを起こしてみよう。
— セッション1
BEGIN;
UPDATE accounts SET balance = 100 WHERE id = 1;
— ここで少し待機して、セッション2がid=2をロックするのを待つ
— セッション2
BEGIN;
UPDATE accounts SET balance = 200 WHERE id = 2;
— セッション1がid=2を更新しようとするとデッドロック発生
こうなったとき、PostgreSQLのログにはこんな風に残るはずだ。
> ERROR: deadlock detected
> DETAIL: Process 1234 waits for ShareLock on transaction 5678; blocked by process 5679.
ここで重要なのは、「どのクエリが原因でロック競合が起きたか」を特定することだ。`pg_stat_activity` を覗いてみるのが一番の近道だよ。
SELECT pid, wait_event_type, wait_event, query
FROM pg_stat_activity
WHERE wait_event_type = ‘Lock’;
—
4. 先輩からのアドバイス:デッドロックを防ぐための「黄金律」
現場でデッドロックを防ぐために、僕がいつも若手に伝えているのはこの3つだ。
1. ロックの順序を統一する: どのテーブルをどの順番で更新するか、アプリ側でルールを徹底する。例えば「idの小さい順から更新する」だけで、デッドロックの発生率は劇的に下がる。
2. トランザクションを短くする: 処理が長ければ長いほど、ロックを保持する時間も長くなる。不要な `SELECT` や外部API呼び出しをトランザクション内に入れないこと。
3. インデックスを適切に張る: インデックスがない状態で `UPDATE` すると、行ロックではなくテーブル全体にロック(あるいは範囲ロック)がかかりやすくなる。これがデッドロックの最大の温床だ。
—
最後に
デッドロックは「悪」じゃない。システムがデータの整合性を守ろうと必死に戦った結果の「防波堤」なんだ。
もしログにデッドロックが出ても、慌てずに「どの処理とどの処理がぶつかっているのか」を冷静に追いかけてみてほしい。
仕組みを知れば、データベースはただの「黒い箱」から「頼もしい相棒」に変わる。
また何か詰まったら、いつでも聞きに来てくれ。一緒にクエリプランを眺めようじゃないか。
コメント