PostgreSQLの「重量級ロック」と上手く付き合う:現場で役立つロック管理の勘所
こんにちは。日々の運用やパフォーマンスチューニングでPostgreSQLと格闘している皆さん、お疲れ様です。
今日は、PostgreSQLのアーキテクチャの中でも、特に「避けては通れないけど、深入りすると沼」になりがちなLock Manager(重量級ロック)についてお話ししようと思います。
公式ドキュメントには「トランザクションレベルの制御」なんて書いてありますが、現場で遭遇するのはそんな綺麗事ばかりじゃありませんよね。「なぜかクエリがブロックされる」「デッドロックが発生した!」という緊急事態の時、この仕組みを知っているかどうかで、復旧のスピードと精神的な余裕が全く変わってきます。
—
「重量級ロック」ってそもそも何者?
PostgreSQLには大きく分けて「軽量級ロック(LWLock)」と「重量級ロック(Heavyweight Lock)」がありますが、今回扱うのは後者です。
簡単に言えば、「データベースのオブジェクト(テーブル、行、インデックスなど)に対して、トランザクション単位で『今は僕が使ってるから触らないでね』と宣言する仕組み」のことです。
このロックの厄介なところは、「デッドロック検出」の対象になるという点。誰かがロックを握ったまま離さないと、待機しているセッションがどんどん増えていき、最終的にDB全体が応答不能になる……なんて悪夢のシナリオは、このロックの管理ミスから生まれます。
—
知っておくべき「ロックの強弱」
ロックにはいくつか種類がありますが、全てのロックが「完全に排他」なわけではありません。ここを勘違いしていると、無駄なパフォーマンス低下を招きます。
- AccessExclusiveLock: 最強のロック。`DROP TABLE`や`ALTER TABLE`で発生。これが出ると他のあらゆるアクセスが止まります。本番環境でやらかすと胃が痛くなるやつですね。
- RowExclusiveLock: `INSERT`, `UPDATE`, `DELETE`で発生。行レベルでロックしますが、テーブル全体には「変更中ですよ」という軽い合図を出す程度なので、並列性は高いです。
- ShareLock: `SELECT … FOR UPDATE`などで発生。読み込みはいいけど、変更は許さないよ、という状態。
「競合するロック」については、PostgreSQLの公式ドキュメントにある[ロックモードの互換性マトリクス](https://www.postgresql.jp/document/current/html/explicit-locking.html#LOCKING-TABLES)を一度は印刷してデスクの横に貼っておくことを強くおすすめします。
—
実践:こんな時、どうロックを制御する?
例えば、「在庫の引き当て」のような処理を想像してみてください。
— トランザクション開始
BEGIN;
— 特定の商品の行をロックして読み込む
SELECT stock_count FROM products WHERE id = 123 FOR UPDATE;
— ここで在庫計算などのロジックを実行…
— 在庫を更新
UPDATE products SET stock_count = stock_count – 1 WHERE id = 123;
COMMIT;
ここで使っている `FOR UPDATE` が重要です。これがないと、トランザクションの途中で別のセッションが同じ行を書き換えてしまい、いわゆる「ロストアップデート(更新の消失)」が発生します。
現場からのアドバイス:
1. ロックの範囲を絞る: テーブル全体をロックするようなクエリは極力避ける。インデックスが効く条件で、可能な限り細かくロックをかけるのが鉄則です。
2. ロックの順序を統一する: デッドロックの最大の原因は「ロックを取る順番がセッションごとにバラバラ」なことです。常に「IDが小さい順に処理する」といったルールをアプリ側に持たせるだけで、デッドロックの発生率は劇的に下がります。
3. タイムアウトを設定する: `SET lock_timeout = ‘5s’;` のように、トランザクションごとにタイムアウトを設定しておきましょう。「いつまでも待機し続けてスレッドが枯渇する」という最悪の事態を防げます。
—
ロックの状態を確認する「魔法のクエリ」
トラブルが起きた時、まず見るべきは `pg_locks` ビューです。今の状況を可視化しないと、闇雲に再起動なんてしても意味がありません。
SELECT
a.pid,
a.query,
l.locktype,
l.mode,
l.granted
FROM pg_stat_activity a
JOIN pg_locks l ON a.pid = l.pid
WHERE NOT l.granted;
このクエリを叩いて、`granted = false` になっているセッションを探してください。誰がどのロックを待っているのか、犯人が一発でわかります。
—
まとめ:恐れず、でも敬意を払う
重量級ロックは、PostgreSQLのデータ整合性を守るための「守護神」です。嫌がる必要はありません。むしろ、この仕組みがあるからこそ、私たちは安心してトランザクションを扱えています。
「なぜこのクエリが遅いのか?」「なぜデッドロックしたのか?」を突き詰めていくと、必ずロックの構造にたどり着きます。その時、マニュアルを眺めるだけでなく、実際に `pg_locks` を叩いて「今、誰が誰の邪魔をしているのか」というストーリーを想像してみてください。
そうやって現場で一つずつ経験を積んでいくのが、最強のDBエンジニアへの近道だと僕は思っています。
また何か詰まったら、いつでも聞きに来てくださいね。現場からは以上です!
コメント