トランザクション操作時の制限と潜在的な問題: LOCK, DEADLOCK
PostgreSQLのトランザクションは、ACIDの重要な特徴の一つである分離性(isolation)を保証してくれる。そのためにシステムはロックを使うんだ。ロックっていうのは、複数のトランザクションが同じレコードやテーブルを「取り合い」しないようにする仕組み。スーパーのレジの列を想像してみて。一度に一人しか会計できないよね。ロックも同じで、テーブルのデータへのアクセスを管理してる感じ。
主なロックの種類
PostgreSQLにはいくつかロックのタイプがある。もしかしたらEXPLAIN ANALYZEで見たことあるかも:
ROW EXCLUSIVE(行排他ロック) — 行データを変更するときに発生する。一番よく使われるロックタイプで、たとえばINSERT、UPDATE、DELETEの時に使われる。SHARE(共有ロック) — クエリ実行中にデータが変更されないことを保証したいときに使う。たとえばSELECT ... FOR SHAREの時とか。EXCLUSIVE(排他ロック) — 読み取り以外の全ての操作をブロックする。
ロックは、データを守る番犬みたいなもんだって思ってくれてOK。
ロックの問題点:何がうまくいかなくなる?
ロックが何か分かったところで、どんな問題が起きるか見てみよう。一番大きいのはデッドロック問題。デッドロック(deadlock)ってのは、2つ以上のトランザクションがお互いを待ち続けて、永遠に進まなくなる状況のこと。典型的にはこんな感じ:
- トランザクション1が行Aをロックして、行Bにアクセスしようとする。
- トランザクション2が行Bをロックして、行Aにアクセスしようとする。
- 両方の行がロックされてるから、どっちのトランザクションも終われない。
デッドロックの例
デッドロックにぶち当たったプログラマーの苦しみを感じてみて。こんなシナリオ:
-- トランザクション1
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 同時に
-- トランザクション2
BEGIN;
UPDATE accounts SET balance = balance + 200 WHERE id = 2;
-- トランザクション1がid = 2をロックしようとする
UPDATE accounts SET balance = balance - 100 WHERE id = 2;
-- トランザクション2がid = 1をロックしようとする
UPDATE accounts SET balance = balance + 200 WHERE id = 1;
ほらね!それぞれのトランザクションが相手にロックされてる。PostgreSQLはそのうちこれを検出して、デッドロックエラーを投げてくる。
デッドロックをどう回避する?
- データアクセスの順番を統一しよう。 いつも同じ順番でデータにアクセスするようにしよう。もしトランザクション1が最初に行A、次に行Bをロックするなら、トランザクション2も同じ順番(まず行A、次に行B)でアクセスするべき。
-- 正しいアクセス順
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 200 WHERE id = 2;
COMMIT;
トランザクションの長さを最小限にしよう。 長いトランザクションはデッドロックのリスクを高める。必要な変更を全部まとめて
BEGINして、すぐにCOMMITしよう。トランザクション分離レベルを使い分けよう。 もし厳密なデータ分離が必要ないなら、
READ COMMITTEDレベルを使うのもアリ。これで一部のロックを回避できる。
タイムアウトとロック診断
PostgreSQLにはロックの診断や回避のためのツールがある。たとえば、ロックの最大待ち時間を設定できる:
SET lock_timeout = '5s'; -- ロックタイムアウトを5秒に設定
ロックが指定時間以上続いたら、そのトランザクションは中断される。これで完全な行き詰まりを防げる。
PostgreSQLでロックを監視する
ロック監視に便利なコマンドの一つがpg_locks。これでシステム内の全てのアクティブなロックが見える:
SELECT * FROM pg_locks;
どのトランザクションがロックを保持していて、どれが解除待ちか分かる。デッドロックのデバッグにも超便利。
LOCKの特徴と手動ロック
手動でロックを管理したい場合は、LOCKコマンドを使おう:
LOCK TABLE orders IN ACCESS EXCLUSIVE MODE;
注意:ACCESS EXCLUSIVEは最強のロックで、SELECTすらできなくなる。他の操作を全部ブロックするから、テーブル構造を変える時とか特別な場合だけ使おう。
特定の行だけロックしたい時はSELECT ... FOR UPDATEを使う:
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
これで他のトランザクションがその行を変更できなくなる。自分のトランザクションが終わるまで安心。
ロックで覚えておくべきこと
ロックは悪者じゃなくてツール。データの整合性を守るためのものだけど、使い方には注意が必要。ポイントは:
- トランザクションが本当に必要か分からないなら始めない。
- できるだけ早くトランザクションを終わらせる。
- データアクセスの順番をバラバラにしない。
- 自分の用途に合った分離レベルを選ぶ。
これでロックやデッドロックとも上手く付き合えるはず!pg_locksとACIDの知恵が君と共にあらんことを。
GO TO FULL VERSION