はじめに
この記事では、PostgreSQLのロック機構の内部実装を深掘りします。
前回の記事(InnoDB ロック機構の内部構造を徹底解説)ではMySQLのロックを解説しました。PostgreSQLはInnoDBと異なり、Gap Lockがありません。代わりにSSI(Serializable Snapshot Isolation)という独自のアプローチでファントムリードを防ぎます。PostgreSQL 17を中心に解説します。
ロックの階層
PostgreSQLのロックは3つのレイヤーに分かれます:
| レイヤー | 名称 | 粒度 | 用途 |
|---|---|---|---|
| 重量級ロック | Regular Lock | テーブル/行 | DDL、明示的ロック |
| 軽量ロック | LWLock | 共有メモリ構造 | 内部データ構造の保護 |
| Spinlock | Spinlock | 極短時間 | LWLockの取得待ち等 |
ユーザーが意識するのは主にRegular Lockです。
テーブルレベルロック
PostgreSQLは8種類のテーブルロックモードを持ちます(InnoDBは実質IS/IX/S/Xの4種類)。
| モード | 典型的な操作 | 競合する主なモード |
|---|---|---|
| ACCESS SHARE | SELECT | ACCESS EXCLUSIVE |
| ROW SHARE | SELECT FOR UPDATE/SHARE | EXCLUSIVE, ACCESS EXCLUSIVE |
| ROW EXCLUSIVE | INSERT/UPDATE/DELETE | SHARE, EXCLUSIVE, ACCESS EXCLUSIVE |
| SHARE | CREATE INDEX (non-concurrent) | ROW EXCLUSIVE, EXCLUSIVE |
| ACCESS EXCLUSIVE | ALTER TABLE, DROP | 全モード |
-- 明示的なテーブルロック
LOCK TABLE employees IN SHARE MODE;
-- 現在のロック確認
SELECT * FROM pg_locks WHERE relation = 'employees'::regclass;InnoDBとの違い:PostgreSQLはDDL操作ごとに細かくロックモードが分かれています。例えばCREATE INDEX CONCURRENTLYはSHARE UPDATE EXCLUSIVEという弱いロックで、DMLをブロックしません。
行レベルロック
InnoDBとの根本的な違い
InnoDBの行ロックはインデックスレコードにかかります。PostgreSQLの行ロックはタプル(行)自体にかかります。
InnoDB: インデックスのレコードにロック → Gap Lock、Next-Key Lockが可能
PostgreSQL: Heapのタプルにロック → Gap Lockの概念がない
4種類の行ロック
| モード | SQL | 用途 |
|---|---|---|
| FOR UPDATE | SELECT ... FOR UPDATE |
更新予定の行を排他ロック |
| FOR NO KEY UPDATE | UPDATE(PKを変更しない) |
外部キーチェックと共存可能 |
| FOR SHARE | SELECT ... FOR SHARE |
共有ロック |
| FOR KEY SHARE | 外部キーチェック時 | FOR NO KEY UPDATEと共存可能 |
InnoDBにはないFOR NO KEY UPDATEとFOR KEY SHAREがあります。これにより、外部キー制約のチェックと通常のUPDATEが互いにブロックしにくくなっています。
行ロックの実装:タプルヘッダ内
PostgreSQLの行ロックはタプルヘッダのxmaxを利用します。ロック専用の領域を持たず、MVCCの仕組みを再利用しています。
タプルヘッダ:
xmax = ロックを保持するTxID
t_infomask: HEAP_XMAX_LOCK_ONLY フラグ(削除ではなくロックであることを示す)
InnoDBが別途ロックテーブル(lock_sys)を持つのに対し、PostgreSQLはタプル自体にロック情報を埋め込むのが特徴です。
Gap Lockがない:ファントムリードはどう防ぐ?
InnoDBはGap Lock / Next-Key Lockでファントムリードを防ぎます。PostgreSQLにはGap Lockがありません。
REPEATABLE READレベル
PostgreSQLのREPEATABLE READはMVCC(Snapshot)だけで実現します。Snapshot取得時点のデータしか見えないため、ファントムリードはそもそも発生しません。
Tx A (REPEATABLE READ):
SELECT * FROM t WHERE id BETWEEN 3 AND 7; → 結果: {5}
Tx B: INSERT INTO t VALUES (4); COMMIT;
Tx A:
SELECT * FROM t WHERE id BETWEEN 3 AND 7; → 結果: {5}(Snapshotが同じ)
ただし、これは「見えない」だけで「防いでいる」わけではありません。Write Skew異常は発生し得ます。
SERIALIZABLEレベル:SSI
PostgreSQLのSERIALIZABLEはSSI(Serializable Snapshot Isolation)で実装されています。ロックではなく、依存関係の検出で直列化可能性を保証します。
SSIの仕組み:
1. 各トランザクションの読み取り/書き込みの依存関係を追跡
2. 直列化不可能な依存パターン(dangerous structure)を検出
3. 検出したらトランザクションをアボート
InnoDBのSERIALIZABLE:
1. 全SELECTがSELECT ... FOR SHARE相当
2. ロックで直列化を強制
| InnoDB | PostgreSQL | |
|---|---|---|
| SERIALIZABLE方式 | ロックベース(全SELECTにSロック) | SSI(依存関係検出) |
| 並列性 | 低い(読み取りが書き込みをブロック) | 高い(ロックなし、事後検出) |
| コスト | ロック待ち | アボート+リトライ |
Advisory Lock
PostgreSQL固有の機能で、アプリケーション定義のロックです。
-- セッションレベルのAdvisory Lock
SELECT pg_advisory_lock(12345); -- 取得
SELECT pg_advisory_unlock(12345); -- 解放
-- トランザクションレベル
SELECT pg_advisory_xact_lock(12345); -- トランザクション終了時に自動解放
-- 非ブロッキング(取得できなければfalseを返す)
SELECT pg_try_advisory_lock(12345);用途: – 外部リソースの排他制御(ファイル、API等) – アプリケーションレベルのキューイング – 重複処理の防止
InnoDBにはGET_LOCK()関数がありますが、PostgreSQLのAdvisory Lockの方が高機能です(共有/排他、セッション/トランザクションレベルの選択が可能)。
デッドロック検出
PostgreSQLもInnoDBと同じくWait-for Graphでデッドロックを検出します。
-- デッドロック検出の待機時間(デフォルト1秒)
SET deadlock_timeout = '1s';InnoDBが即座に検出するのに対し、PostgreSQLはdeadlock_timeoutの間待ってから検出を開始します。多くの場合、タイムアウト前にロックが解放されるため、検出コストを節約できます。
まとめ
| トピック | InnoDB | PostgreSQL |
|---|---|---|
| 行ロックの対象 | インデックスレコード | タプル自体 |
| Gap Lock | あり | なし |
| ファントムリード防止 | Next-Key Lock | MVCC + SSI |
| SERIALIZABLE | ロックベース | SSI(依存関係検出) |
| 行ロックの種類 | S, X | FOR UPDATE/SHARE/NO KEY UPDATE/KEY SHARE |
| テーブルロック | 4種類 | 8種類 |
| Advisory Lock | GET_LOCK() | pg_advisory_lock(高機能) |
| デッドロック検出 | 即座 | deadlock_timeout後 |
次回はOracle ロック機構を解説します。