
【PostgreSQL】デッドロックを発生させてエラーメッセージとトランザクションの中断状態を確認してみた
はじめに
PostgreSQLでロック待ちが発生することは珍しくありませんが、
2つのトランザクションが互いのロックを待ち合うと「デッドロック」という特殊な状態になります。
今回は実際にデッドロックを発生させ、エラーメッセージの読み方や、アボートされた側のトランザクションがどうなるのかを検証してみました。
いきなりまとめ
- デッドロックは、2つのトランザクションが互いにロックの解放を待ち合うことで発生する
- PostgreSQLはこの状態を自動検知し、どちらか一方を強制的にアボートすることで解消する
- アボートされた側のトランザクションは、ROLLBACK するまで何も実行できない状態になる
検証環境
検証のための実行環境は EC2(AL2023, t3.micro)にインストールした PostgreSQL を使用しました。
EC2 に PostgreSQL をインストールする手順としては下記をご参照ください。
PostgreSQL 17.10 を利用し、検証していきます。
[ec2-user@ip-xx-xx-xx-xx ~]$ sudo -u postgres psql
psql (17.10)
Type "help" for help.
postgres=# SELECT version();
version
---------------------------------------------------------------------------------------------------------------
PostgreSQL 17.10 on x86_64-amazon-linux-gnu, compiled by gcc (GCC) 11.5.0 20240719 (Red Hat 11.5.0-5), 64-bit
(1 row)
サンプルデータ準備
検証用にデータベースとテーブルを作成します。
postgres=# CREATE DATABASE blog_sample_db;
CREATE DATABASE
postgres=# \c blog_sample_db
You are now connected to database "blog_sample_db" as user "postgres".
blog_sample_db=# CREATE TABLE lock_test (id INT PRIMARY KEY, val TEXT);
CREATE TABLE
blog_sample_db=# INSERT INTO lock_test VALUES (1, 'a'), (2, 'b');
INSERT 0 2
blog_sample_db=# select * from lock_test;
id | val
----+-----
1 | a
2 | b
(2 rows)
デッドロックを発生させる
ターミナルを2つ(セッションA, セッションB)用意し、デッドロックを再現していきます。
まず、セッションA で id=1 の行を UPDATEします。
-- プロセスID確認
blog_sample_db=# SELECT pg_backend_pid();
pg_backend_pid
----------------
5184
(1 row)
-- トランザクション開始
blog_sample_db=# BEGIN;
BEGIN
-- トランザクションIDを確認
blog_sample_db=*# SELECT txid_current();
txid_current
--------------
76461
(1 row)
-- 更新処理
blog_sample_db=*# UPDATE lock_test SET val = 'x' WHERE id = 1;
UPDATE 1
(→ ここで止める(まだ次のクエリを打たない))
続いて、セッションB にて id=2 および id=1 の行の更新を試みます。
blog_sample_db=# SELECT pg_backend_pid();
pg_backend_pid
----------------
5189
(1 row)
blog_sample_db=# BEGIN;
BEGIN
blog_sample_db=*# SELECT txid_current();
txid_current
--------------
76462
(1 row)
blog_sample_db=*# UPDATE lock_test SET val = 'y' WHERE id = 2;
UPDATE 1
blog_sample_db=*# UPDATE lock_test SET val = 'z' WHERE id = 1;
(セッションAが id=1 の行を更新中なので固まる。(セッションAがid=1を持っているため待機))
上記の通り、id=2 は更新ができますが、id=1 はセッションA のトランザクション内で更新中のため、処理待ちに入ります。
再度セッションA に戻り、id=2 の行を更新してみます。
するとデッドロックエラーが検知されました。
blog_sample_db=*# UPDATE lock_test SET val = 'w' WHERE id = 2;
ERROR: deadlock detected
DETAIL: Process 5184 waits for ShareLock on transaction 76462; blocked by process 5189.
Process 5189 waits for ShareLock on transaction 76461; blocked by process 5184.
HINT: See server log for query details.
CONTEXT: while updating tuple (0,2) in relation "lock_test"
上記のエラー内容より、Process 5184(セッションA) は transaction 76462(セッションB) のロック解放を待ち、Process 5189(セッションB) は transaction 76461(セッションA) のロック解放を待っている、つまり互いが互いを待って動けない状態となっていたことが分かります。
この状態をPostgreSQLが自動検知し、今回はセッションA 側を強制的にエラーにすることでデッドロックを解消しています。
PostgreSQLでは、自動的にデッドロック状況を検知し、関係するトランザクションの一方をアボートすることにより、この状況を解決し、もう一方のトランザクションの処理を完了させます (どちらのトランザクションをアボートするかを正確に予期するのは難しく、これに依存すべきではありません)。
13.3. 明示的ロック
なお、上記セッションA のデッドロックエラー直後、セッションBの止まっていた UPDATE が以下のように動き出します。
blog_sample_db=*# UPDATE lock_test SET val = 'z' WHERE id = 1;
UPDATE 1
blog_sample_db=*#
ちなみに、デッドロックとなりトランザクションがアボートされた側(セッションA)でのその後のクエリ実行は、エラーとなりできません。
blog_sample_db=!# SELECT * FROM lock_test;
ERROR: current transaction is aborted, commands ignored until end of transaction block
トランザクションがアボートされた後の戻し方は以下ドキュメントに記述があります。
さらには何らかのエラーでシステムがトランザクションブロックを中断状態にした場合、完全にロールバックして再び開始するのを別とすれば、ROLLBACK TOコマンドがトランザクションブロックの制御を取り戻す唯一の手段です。
3.4. トランザクション
そのため、セッションA のトランザクションは ROLLBACK して元の状態に戻しておきます。
blog_sample_db=!# ROLLBACK;
ROLLBACK
blog_sample_db=# SELECT * FROM lock_test;
id | val
----+-----
1 | a
2 | b
(2 rows)
セッションB のトランザクションは現在も生きているので、COMMIT して中身を確認します。
結果より、セッション B のトランザクション内での変更が反映されていることがわかります。
blog_sample_db=*# COMMIT;
COMMIT
blog_sample_db=# SELECT * FROM lock_test;
id | val
----+-----
2 | y
1 | z
(2 rows)
セッションA側でも見てみましょう。セッションBのトランザクションの更新が反映されていますね。
blog_sample_db=# SELECT * FROM lock_test;
id | val
----+-----
2 | y
1 | z
(2 rows)
各セッション処理の全体
blog_sample_db=# SELECT pg_backend_pid();
pg_backend_pid
----------------
5184
(1 row)
blog_sample_db=# BEGIN;
BEGIN
blog_sample_db=*# SELECT txid_current();
txid_current
--------------
76461
(1 row)
blog_sample_db=*#
blog_sample_db=*# UPDATE lock_test SET val = 'x' WHERE id = 1;
UPDATE 1
blog_sample_db=*# UPDATE lock_test SET val = 'w' WHERE id = 2;
ERROR: deadlock detected
DETAIL: Process 5184 waits for ShareLock on transaction 76462; blocked by process 5189.
Process 5189 waits for ShareLock on transaction 76461; blocked by process 5184.
HINT: See server log for query details.
CONTEXT: while updating tuple (0,2) in relation "lock_test"
blog_sample_db=!# SELECT * FROM lock_test;
ERROR: current transaction is aborted, commands ignored until end of transaction block
blog_sample_db=!# ROLLBACK;
ROLLBACK
blog_sample_db=# SELECT * FROM lock_test;
id | val
----+-----
1 | a
2 | b
(2 rows)
blog_sample_db=# SELECT * FROM lock_test;
id | val
----+-----
2 | y
1 | z
(2 rows)
blog_sample_db=# SELECT pg_backend_pid();
pg_backend_pid
----------------
5189
(1 row)
blog_sample_db=# BEGIN;
BEGIN
blog_sample_db=*# SELECT txid_current();
txid_current
--------------
76462
(1 row)
blog_sample_db=*# UPDATE lock_test SET val = 'y' WHERE id = 2;
UPDATE 1
blog_sample_db=*# UPDATE lock_test SET val = 'z' WHERE id = 1;
UPDATE 1
blog_sample_db=*# COMMIT;
COMMIT
blog_sample_db=# SELECT * FROM lock_test;
id | val
----+-----
2 | y
1 | z
(2 rows)
終わりに
今回はデッドロックを実際に発生させ、エラーメッセージの読み方やアボートされたトランザクションの挙動を確認しました。
デッドロックは実務でも遭遇することがあるため、エラーメッセージからどのプロセス・トランザクションが待ち合っているのかを読み解けるようになっておくと、原因調査がスムーズになるかと思います。
本記事がどなたかのお役に立てば幸いです。
参考情報











