これは PostgreSQL Advent Calendar 2024 2 枚目・10 日目の記事です。
5 日目に少しだけ試した Aurora DSQL(プレビュー版)は同時実行制御として楽観的同時実行制御(OCC)を採用していますが、この挙動を確認してみました。
悲観的同時実行制御(PCC)と楽観的同時実行制御(OCC)
通常の RDBMS では、悲観的同時実行制御(PCC : Pessimistic Concurrency Control)によるトランザクション処理を採用しています。
PCC では、ロックを使って書き込みの競合を管理します。
例えば、先行するトランザクションが行挿入を行う一方で後続のトランザクションが同一の主キー値で行挿入を行った場合、後続トランザクションは先行トランザクションがコミットまたはロールバックされるまで待たされます。そして先行トランザクションがコミットした場合は主キー重複エラーが発生、ロールバックした場合は行挿入が成功します。
一方、Aurora DSQL は楽観的同時実行制御(OCC : Optimistic Concurrency Control)を採用しています。
OCC では、トランザクションの途中ではロックを使わず処理を進め、コミットの段階で書き込み競合を判定します。
先ほどの例と同様に、先行するトランザクションが行挿入を行う一方で後続のトランザクションが同一の主キー値で行挿入を行った場合、先行トランザクションがコミットまたはロールバックするのを待つことなく後続トランザクションで行挿入が実行されます。そして、いずれかのトランザクションのうち、先にコミットした側が成功し、後からコミットした側が(変更の競合エラーで)失敗します。
Amazon Aurora DSQL の同時実行制御について、5 日目の記事には AWS 公式ブログの英語記事へのリンクを示しましたが、その後日本語訳が公開されたので、あらためて日本語ブログ記事へのリンクを示します。
まずは通常の PostgreSQL でトランザクションの動作を確認する
サーバーとして PostgreSQL 16 を使って試してみます。
今回は(動作がわかりやすいので)行挿入の動作を確認・比較します。更新の場合は違う動作になります。
テーブルを準備
まずスキーマとテーブルを準備します。
postgres=# CREATE SCHEMA hoge;
CREATE SCHEMA
postgres=# CREATE TABLE hoge.fuga(id INT PRIMARY KEY UNIQUE, val INT);
CREATE TABLE
1 つ目のトランザクションで行挿入
引き続き、トランザクションを開始して主キーが1
の行を挿入します。
postgres=# BEGIN;
BEGIN
postgres=*# INSERT INTO hoge.fuga VALUES(1, 100);
INSERT 0 1
2 つ目のトランザクションで行挿入
別のクライアント(psql
)で 2 つ目のトランザクションを開始して、主キーが(重複するように)1
の行を挿入します。
postgres=# BEGIN;
BEGIN
postgres=*# INSERT INTO hoge.fuga VALUES(1, 200);
ロック待ち状態になりました。
1 つ目のトランザクションでCOMMIT
1 つ目のトランザクションに戻ってCOMMIT
します。
postgres=*# COMMIT;
COMMIT
COMMIT
が成功しました。
そして、2 つ目のトランザクションでエラーが発生しました。
ERROR: duplicate key value violates unique constraint "fuga_pkey"
DETAIL: Key (id)=(1) already exists.
2 つ目のトランザクションはエラーが出た状態で継続しているのでROLLBACK
します。
postgres=!# ROLLBACK;
ROLLBACK
DSQL でトランザクションの動作を確認する
前回の記事のとおりに複数リージョンのクラスターを作成します。
(手順は省略)
テーブルを準備
先ほどと同様にスキーマとテーブルを準備します。
postgres=# CREATE SCHEMA hoge;
CREATE SCHEMA
postgres=# CREATE TABLE hoge.fuga(id INT PRIMARY KEY UNIQUE, val INT);
CREATE TABLE
バージニア北部リージョンで 1 つ目のトランザクションを開始し行挿入
CloudShell でpsql
を使って DSQL クラスターのバージニア北部リージョンに接続します。
1 つ目のトランザクションを開始し主キーが1
の行を挿入します。
postgres=> BEGIN;
BEGIN
postgres=*> INSERT INTO hoge.fuga VALUES(1, 100);
INSERT 0 1
ここまでは同じですね。
オハイオリージョンで 2 つ目のトランザクションを開始し行挿入
CloudShell でpsql
を使って DSQL クラスターのオハイオリージョンに接続します。
2 つ目のトランザクションを開始し主キーが(重複する)1
の行を挿入します。
postgres=> BEGIN;
BEGIN
postgres=*> INSERT INTO hoge.fuga VALUES(1, 200);
INSERT 0 1
先ほどのようにロック待ちにはならずINSERT
が実行されました。
バージニア北部リージョンでCOMMIT
バージニア北部リージョンでCOMMIT
してみます。
postgres=*> COMMIT;
COMMIT
COMMIT
が成功しました。
この時点ではオハイオリージョンのトランザクションに変化は見られません。
オハイオリージョンでCOMMIT
オハイオリージョンでCOMMIT
してみます。
postgres=*> COMMIT;
ERROR: change conflicts with another transaction, please retry: (OC000)
この時点で初めてエラー(変更の競合)が出ました。
postgres=> ROLLBACK;
WARNING: there is no transaction in progress
ROLLBACK
プロンプトを見るとわかりますが、すでに 2 つ目のトランザクションは終了しているのでROLLBACK
は不要です。
2 つのトランザクションを同一リージョンで実行しても同様の(OCC に沿った)動作になります。
これ以外のパターンは?
先の公式ブログ記事に、更新処理やSELECT ... FOR UPDATE
、DDL を使うケースが例 1・例 2・例 3 として示されています。
変更の競合でエラーが発生したら?
エラーメッセージにもありますが、基本的にはエラーが発生した側のトランザクション処理をリトライ(最初からやり直し)する形で対処します。
現実には行挿入時に主キーが重複してリトライが発生するような設計のテーブルを扱うケースは少なく、ユニークキーや更新時の競合でリトライするケースが多いと思います。
明日(11 日目)の 2 枚目カレンダーの担当は jun2 さんです。