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

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

# 前提知識
主にPostgreSQLのドキュメントをもとに前提知識を整理しましょう。
## テーブルレベルロック
テーブル単位でのロック制御を行います。[13.3. Explicit Locking](https://www.postgresql.org/docs/13/explicit-locking.html)をもとにまとめたのが下記で
`SHARE ROW EXCLUSIVE`/`EXCLUSIVE`がグレーアウトされているのは比較的マイナーなので基本的に覚えなくていいと思っているからです
左端に追記されているコマンド群はPostgresSQLが内部的に該当のロックを利用することを表現しています。なお、`ALTER TABLE`は一部`ACCESS EXCLUSIVE`以外を利用しますがかなり少数派なので無視します。

![Untitled Diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370237/e135b639-000c-2eb3-46d6-db18d0116a57.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](https://www.postgresql.org/docs/13/explicit-locking.html)をもとに以下にまとめています。4つのモードが利用可能なようですが、あまり意識することもないと思います。行レベルロックにもモードが複数あるぞくらいの認識でいいと思ってます。

![Untitled Diagram-Page-2 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370237/b91743c4-f98c-a85d-5551-da56d7fa6514.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](https://www.postgresql.org/docs/13/view-pg-locks.html)

>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](https://www.postgresql.org/docs/13/pgrowlocks.html)にて確認できるようです。[^1]が、基本的にはアプリケーションエンジニアが利用するケースはほとんどないでしょう。

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


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

[^2]: PostgreSQLの内部構造を手軽に理解するには[PostgreSQL全機能バイブル](https://www.amazon.co.jp/-/en/%E9%88%B4%E6%9C%A8-%E5%95%93%E4%BF%AE/dp/4774153923/)の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_infomask`、`t_infomask2`にはビット情報を保持し、

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

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

# 行ロックはどのように動作するか
基本的には[冒頭の記事](https://masahikosawada.github.io//2018/09/04/Row-locking-and-locking-on-transactionid/)を読んでください。
簡単に説明すると、

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

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

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

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

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

[^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 のあたりに記載を見つけた

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

```sql

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;
```

調査のためのテーブル

```sql

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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370237/0d614f4f-c94c-5c08-32c3-5e5958cc1b1c.png)

3718が`select for update`
![スクリーンショット 2021-03-16 2.53.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370237/dbb982eb-fc65-d9f3-909d-e9c8f6ca6706.png)

3721が`select for update`
![スクリーンショット 2021-03-16 2.46.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370237/f910d8eb-8be7-dc98-22dd-90eec3fc6388.png)

3734が`select for update`
![スクリーンショット 2021-03-16 2.46.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370237/a67185ce-5e87-2dda-b0e5-3a13f474de87.png)

3718がcommit
![スクリーンショット 2021-03-16 2.47.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370237/678bbd45-f7ce-4953-1bb0-41851b1c91a9.png)

3721がcommit
![スクリーンショット 2021-03-16 2.47.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370237/7f4907ed-124e-dec5-9e07-3308fc0b8b13.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370237/19aac059-5e8a-9d2c-b1cf-d46e40500637.png)

pid=3829がid=1とid=2をselect for update
![スクリーンショット 2021-03-16 3.19.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370237/93d9ff40-3ca4-596b-deac-dfe52cbf73de.png)

pid=3827がcommit
![スクリーンショット 2021-03-16 3.19.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370237/b79e6912-4e08-55e2-92ef-75ec942cb9a5.png)


pid=3828がcommit
![スクリーンショット 2021-03-16 3.19.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370237/a65cb7ad-4eff-ac12-a6de-5795586447f8.png)

SELECT * FROM pgrowlocks('users');
![スクリーンショット 2021-03-16 3.20.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370237/51218171-f539-4fdb-6047-f9dbec6c29d3.png)

わかることのメモ

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