PostgreSQL ロック機構の内部構造を徹底解説:行ロック、Advisory Lock、SSI まで

はじめに

この記事では、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 CONCURRENTLYSHARE 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 UPDATEFOR 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 ロック機構を解説します。

参考文献

コメントする