0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【トランザクションの基本】データが変わる?増える?「ノンリピータブルリード」と「ファントムリード」の違いをDockerで検証

0
Posted at

はじめに

PostgreSQLの REPEATABLE READ は非常に優秀で、SQL標準で定義されている「ダーティリード」「ノンリピータブルリード」「ファントムリード」の3大異常現象をすべて防いでくれます。

「じゃあ、最高レベルの SERIALIZABLE はいらないのでは?」
いいえ、そんなことはありません。REPEATABLE READでも防げない、複数のトランザクションが複雑に絡み合った時に起こる「ライトスキュー(Write Skew)」という最後の異常現象が存在します。

この記事では、ライトスキューの恐ろしさと、SERIALIZABLEがそれをどう防ぐのかをDockerを使って実証します。


1. ライトスキュー(Write Skew)とは何か?

一言でいうと、「2つのトランザクションが、お互いに『相手の古いデータ』を見た上で『別々の行』を更新した結果、全体のルールがぶっ壊れる現象」です。

病院の当番システムの例

病院には「アリス」と「ボブ」の2人の当番医がいます。
ルール:「当番(ON)は、常に最低1人はいないといけない」

ある日、アリスとボブが「2人ともON」の状態でした。
ここで、2人が「今日休みたいな…」と思い、同時にシステムを操作した時のタイムラインを見てみましょう。

なぜREPEATABLE READでは防げないのか?

以前の「同時更新エラー」の時は、AとBが「同じ行(同じ口座)」を更新しようとしたからPostgresが弾いてくれました。
しかし今回は、アリスは「アリスの行」を、ボブは「ボブの行」を更新しています。別々の行を更新しているため、行ロック(コンフリクト)が発生せず、両方とも成功してしまうのです。これがライトスキューです。


2. ハンズオン準備(Docker Compose)

実際にDocker環境で、この悲劇を再現してみましょう。
(※すでに前回のコンテナが動いている場合は、そのまま接続してください)

docker compose exec db psql -U user -d testdb
-- 病院の当番テーブルを作成
CREATE TABLE doctors (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    on_call BOOLEAN
);

-- アリスとボブを当番(true)として登録
INSERT INTO doctors VALUES (1, 'Alice', true);
INSERT INTO doctors VALUES (2, 'Bob', true);

3. 実験①:REPEATABLE READの悲劇

ターミナルを2つ(Terminal A, B)並べて開き、同時に操作します。

Step 1: 両方でトランザクションを開始し、状況を確認

-- Terminal A (アリス)
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT count(*) FROM doctors WHERE on_call = true;
-- 結果: 2
-- Terminal B (ボブ)
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT count(*) FROM doctors WHERE on_call = true;
-- 結果: 2

Step 2: それぞれが「相手がいるから大丈夫」と判断して自分を休みにする

-- Terminal A (アリス)
UPDATE doctors SET on_call = false WHERE name = 'Alice';
-- UPDATE 1 (成功)
-- Terminal B (ボブ)
UPDATE doctors SET on_call = false WHERE name = 'Bob';
-- UPDATE 1 (成功)

Step 3: 両方ともコミット

-- Terminal A
COMMIT;
-- Terminal B
COMMIT;

Step 4: 最終確認

SELECT * FROM doctors;
-- Alice: f (false)
-- Bob: f (false)
-- ★ルール崩壊!誰も病院にいなくなりました!

4. 実験②:SERIALIZABLEが世界を救う

データを元に戻して、PostgreSQLの究極の防衛線「SERIALIZABLE」の力を試してみましょう!

-- 事前準備:2人を当番に戻す
UPDATE doctors SET on_call = true;

Step 1: 両方で SERIALIZABLE をセットして開始

-- Terminal A (アリス)
BEGIN;
-- ★最高峰 SERIALIZABLE にする
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT count(*) FROM doctors WHERE on_call = true; -- 結果: 2
-- Terminal B (ボブ)
BEGIN;
-- ★最高峰 SERIALIZABLE にする
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT count(*) FROM doctors WHERE on_call = true; -- 結果: 2

Step 2: それぞれ自分を休みにする

-- Terminal A (アリス)
UPDATE doctors SET on_call = false WHERE name = 'Alice';
-- Terminal B (ボブ)
UPDATE doctors SET on_call = false WHERE name = 'Bob';

ここまでは両方ともエラーなく進行します。

Step 3: 運命のコミット

アリスが先にコミットし、次にボブがコミットしようとすると……

-- Terminal A (アリス)
COMMIT; -- (成功)
-- Terminal B (ボブ)
COMMIT;

ここで Terminal B に強烈なエラーが発生します!

ERROR:  could not serialize access due to read/write dependencies among transactions
DETAIL:  Reason code: Canceled on identification as a pivot, during commit attempt.
HINT:  The transaction might succeed if retried.

(翻訳:「トランザクション間の読み書きの依存関係がおかしいので、直列化(シリアライズ)できませんでした。エラーにします!」)

PostgreSQLは、「別々の行」の更新であっても、「お互いが読んだデータをもとに更新し合っている(依存関係の交差)」ことを裏で完璧に検知し、矛盾が起きる前にボブのコミットを強制的に弾き飛ばしました。
結果として、アリスだけが休みになり、ボブは当番として残るため、病院のルールが守られました!


5. まとめ

  1. REPEATABLE READ の限界:
    自分以外の行を更新し合うような複雑なケース(ライトスキュー)では、行ロックが効かず、データの矛盾(ルール違反)が起きてしまう。
  2. SERIALIZABLE の役割:
    すべてのトランザクションを監視し、「もし1人ずつ順番(直列)に処理していたら、絶対にこんな結果にはならないよね?」という矛盾のニオイを検知した瞬間、エラーを出して処理を弾く究極の安全装置。
  3. 実務での使い分け:
    SERIALIZABLE は安全ですが、エラーで弾かれる確率が高く、システムの処理速度が落ちます。基本は READ COMMITTED を使い、シフト管理や高度な在庫引当など「複数のデータ条件が絡む厳密なルール」がある処理にだけ SERIALIZABLE を使うのがプロの設計です。
0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?