LoginSignup
34
34

More than 3 years have passed since last update.

PostgreSQLの行ロックの挙動を確認する

Last updated at Posted at 2021-03-15

はじめに

PostgreSQLの行ロックの挙動は難しく、ドキュメントにも情報が皆無なので理解を諦めていたが、久しぶりにpg_locks等で調査する必要にせまられ、軽く調べてみたところ、sawadaさんがとても素晴らしい記事を書いていたのを発見したので紹介したい。
この記事だけでは完璧には理解できなかったので、実際にSQLをなげたときの挙動を確かめて理解を深めていきたい。なお、ドキュメントや挙動の確認はバージョン13を元に行っています。

前提知識

主にPostgreSQLのドキュメントをもとに前提知識を整理しましょう。

テーブルレベルロック

テーブル単位でのロック制御を行います。13.3. Explicit Lockingをもとにまとめたのが下記で
SHARE ROW EXCLUSIVE/EXCLUSIVEがグレーアウトされているのは比較的マイナーなので基本的に覚えなくていいと思っているからです
左端に追記されているコマンド群はPostgresSQLが内部的に該当のロックを利用することを表現しています。なお、ALTER TABLEは一部ACCESS EXCLUSIVE以外を利用しますがかなり少数派なので無視します。

Untitled Diagram.png

Remember that all of these lock modes are table-level locks, even if the name contains the word “row”; the names of the lock modes are historical. To some extent the names reflect the typical usage of each lock mode — but the semantics are all the same.

とあるように、lock modeの命名にrowが含まれていてもあくまでテーブルレベルのロックであることに注意しましょう。
テーブルレベルのロック機構は共有メモリにて管理され、deadlockの検出をサポートするなど作り込まれたものであり、PostgreSQL用語でRegular locks (a/k/a heavyweight locks)と表現されることが多いようです。
このlock情報はpg_locksをもとに確認することができます。

行レベルロック

同様に13.3. Explicit Lockingをもとに以下にまとめています。4つのモードが利用可能なようですが、あまり意識することもないと思います。行レベルロックにもモードが複数あるぞくらいの認識でいいと思ってます。

Untitled Diagram-Page-2 (1).png

PostgreSQL doesn't remember any information about modified rows in memory, so there is no limit on the number of rows locked at one time. However, locking a row might cause a disk write, e.g., SELECT FOR UPDATE modifies selected rows to mark them locked, and so will result in disk writes.

とあります。行レベルのロックは、テーブルレベルロックと同様にメモリで管理するわけにはいかないので完全にディスクにて管理されます

51.73. pg_locks

Although tuples are a lockable type of object, information about row-level locks is stored on disk, not in memory, and therefore row-level locks normally do not appear in this view. If a process is waiting for a row-level lock, it will usually appear in the view as waiting for the permanent transaction ID of the current holder of that row lock.

とあるように、pg_locksには行レベルロックは現れません。pg_locksはあくまでテーブルレベルロックのみのようです

行レベルのロックはextensionで利用可能なpgrowlocksにて確認できるようです。1が、基本的にはアプリケーションエンジニアが利用するケースはほとんどないでしょう。

ページレベルロック

こちらはユーザがあまり意識することはなさそうですが、マニュアルにあったので念の為書いてます。ページ単位のロックです。
あまりPostgreSQLに詳しくない人のために補足すると、テーブルは物理的には一つのファイルであり、8Kバイトのブロック単位で分割されて管理されます。データが増えてブロック内に保存しきれなくなると、ファイルの末尾にブロックを追加してファイルサイズを増やします。このブロックをページと呼びます。2
ページ単位で共有メモリにバッファリングされており、ページ単位でロックが存在します。バッファから読み込む時に共有ロックをかけ、バッファに変更を加える時やディスクから読み込む時には排他ロックが利用されます。

ヒープタプルの構造

この記事ではそこまでdeepな話は書くつもりはないですが、ブログを理解するにはある程度事前知識があったほうがいいので軽く触れます。

ヒープタプルとは行レコードを指します。実際のレコードのデータと一緒にそのメタ情報がディスクに隣接して保存されます。HeapTupleHeaderDataと呼ばれます。

ソースとしては
https://github.com/postgres/postgres/blob/master/src/include/access/htup_details.h
のあたりです。
大事なものを一部紹介すると、t_xminにはそのタプルを追記したXID、t_xmaxにはそのタプルを削除、もしくは更新したXIDが保持され(実はロックの時もt_xmaxを利用しますが後ほど紹介)、t_tcidには追記された新しいタプルへのポインタが保持されます。PostgreSQLは追記型ですが、これらの情報を利用することで該当レコードの更新情報を辿ることができたり、コミットログなどと合わせてMVCCを実現しています。
他にはt_infomaskt_infomask2にはビット情報を保持し、

  • 行レベルロックがある場合はどのモードか
  • update/deleteされたか
  • xmin/xmaxはcommit/abortしたのか

など様々な情報を保持します。

行ロックはどのように動作するか

基本的には冒頭の記事を読んでください。
簡単に説明すると、

  • テーブルレベルロックと同様にメモリでロックを管理するわけにはいかないので、ロック対象の行のxmaxに自身のXIDを書き、t_infomaskt_infomask2を更新することで行レベルロックを実現
  • そのままではテーブルレベルロックのマネージャだけではデッドロックを検出できないので、行レベルロックとテーブルレベルロック両方を利用する。
  • 全てのトランザクションは、トランザクション開始時に自身のトランザクションIDに対してExclusiveLockを取得し、そのロックはトランザクション完了時まで保持する。
  • あるトランザクションが特定のトランザクションのロック開放を待つ時、そのトランザクションIDに対してShareLockを要求する。これにより「あるトランザクションが行レベルロックを取得済みのあるトランザクションのロック開放を待っている」ということをテーブルレベルロックのマネージャだけで表現できる。
  • PostgreSQLはロックスケジュールとしてFIFOを採用しているので、行毎にFIFOを実現するべきでそのためには工夫が必要

という背景があり、以下のように動作している。3

  1. 行のxmaxを見て、空もしくはxmaxのトランザクションが終了していればxmaxに自身のトランザクションIDを書いてt_infomaskt_infomask2を更新し、6へ移動。そうでなければ以下続く。
  2. 行へのAccessExclusiveLockを取得
  3. トランザクションID(=今現在のxmaxの値)へのShareLockを取得後すぐ開放
  4. xmaxに自身のトランザクションIDを書き、t_infomaskt_infomask2を更新
  5. 行へのAccessExclusiveLockを開放
  6. そのまま処理を続行し、終了時に自身のトランザクションIDに対してのExclusiveLockが開放される。

ややこしいのですが、1と4のみが行レベルロックで、その他はテーブルレベルロックです。2のテーブルレベルロックに「行」という言葉が含まれるのでややこしいのですが、このロックは取得したい行レベルロックのための仮想的なテーブルレベルロックという理解をした方がいいと思います。取得済みの行レベルロックの情報はテーブルレベルロックには現れません。また、行へのAccessExclusiveLockを同時に2つ以上取得もしくは待機することはありません4。この二つにより、テーブルレベルロックの構造が肥大化することがないことがわかると思います。

上記の仕組みはとても美しいですが、理解するのに時間を要すると思います。逆に理解できていれば次の章の挙動は全て自明に思えると思います。

挙動の確認

以下のコマンドで確認していきます。


select
 l.pid,
 l.locktype,
 c.relname as table_name,
 l.page,
 l.tuple,
 l.transactionid,
 l.mode,
 l.granted, -- trueの場合lock取得済み
 s.state
from
 pg_locks l
 left join pg_class c  on (l.relation=c.oid)
 left join pg_stat_activity s on (l.pid=s.pid)
where
 l.mode <> 'AccessShareLock' -- レコードが多くなり見辛くなるので対象外
 and
 l.pid <> pg_backend_pid()  -- このクエリ実行自体は対象外
order by
 l.pid;

調査のためのテーブル


create table users
(
    id  integer constraint users_pkey primary key,
    user_type   integer
);
INSERT INTO users (id, user_type) VALUES (1,1);
INSERT INTO users (id, user_type) VALUES (2,1);
INSERT INTO users (id, user_type) VALUES (3,1);

基本的な例

id=1を3つのセッションがselect for updateすることを想定し、pidが、3718/3721/3734と順番にselect for updateしていく。

それぞれBEGINだけした様子
スクリーンショット 2021-03-16 2.49.39.png

3718がselect for update
スクリーンショット 2021-03-16 2.53.43.png

3721がselect for update
スクリーンショット 2021-03-16 2.46.36.png

3734がselect for update
スクリーンショット 2021-03-16 2.46.46.png

3718がcommit
スクリーンショット 2021-03-16 2.47.00.png

3721がcommit
スクリーンショット 2021-03-16 2.47.15.png

わかることのメモ

  • 常にusersやそのpkeyに対してのRowShareLockはlock取得済みの状態にある。
  • lock待ちのstateはactiveであり、lock取得ずみのstateはSQL実行中ではないため、idle in transactionである。
  • select id from users where ctid='(0,1)';により、id=1のレコードのtupleが対象であることが確かめられる。
  • 行ロック待ちの最初のトランザクションはトランザクションIDへのShareLock待ち。続いてのトランザクションは行へのAccessExclusiveLock待ちとなる。

複数レコードlockしようとする例

pid=3827がid=1をpid=3828がid=2をlockしている状態でpid=3829がid=1とid=2をlockしようとする状況。

pid=3827がid=1、pid=3828がid = 2をselect for update
スクリーンショット 2021-03-16 3.19.11.png

pid=3829がid=1とid=2をselect for update
スクリーンショット 2021-03-16 3.19.24.png

pid=3827がcommit
スクリーンショット 2021-03-16 3.19.38.png

pid=3828がcommit
スクリーンショット 2021-03-16 3.19.48.png

SELECT * FROM pgrowlocks('users');
スクリーンショット 2021-03-16 3.20.08.png

わかることのメモ

  • lock待ちは1行ずつ
  • pg_locksには現れないが、pgrowlocksで確かに2行ロックしていることが確認できる。

  1. https://qiita.com/aramaki/items/cfef1dfbef2402c485fd に詳しいです。 

  2. PostgreSQLの内部構造を手軽に理解するにはPostgreSQL全機能バイブルの2章がとてもおすすめです。 

  3. 元記事の他にhttps://qiita.com/aramaki/items/5f26c6227ff436a56e2c#%E3%82%BB%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B31%E3%81%AE%E5%8B%95%E3%81%8D も参照しているため元記事と異なる記載が多い。 

  4. https://github.com/postgres/postgres/blob/eeb60e45d82d5840b713a8741ae552238d57e8b9/src/backend/access/heap/README.tuplock#L32-L34 のあたりに記載を見つけた 

34
34
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
34
34