CodeGym /コース /SQL SELF /SERIALIZABLEアイソレーションレベル:完全な分離と Phantom Readの防止

SERIALIZABLEアイソレーションレベル:完全な分離と Phantom Readの防止

SQL SELF
レベル 40, レッスン 2
使用可能

SERIALIZABLEは、PostgreSQLで一番ガチなトランザクションアイソレーションレベルだよ。このレベルだと、並行トランザクションの結果が、まるで順番に(シリアルに)一個ずつ実行されたみたいになる。つまり、並行実行の変な挙動(たとえばDirty Read、Non-Repeatable Read、Phantom Read)は絶対に起きない。

ざっくり言うと、SERIALIZABLEは並行トランザクション同士の順番や一貫性をガッチリ守ってくれる感じ。PostgreSQLが「みんな、トランザクションはちゃんと並んでね!」って言ってるイメージ。

なんでSERIALIZABLEが必要なの?っていうと、どんなに並行でデータがいじられても、絶対にデータが一貫してるって100%安心したいときがあるから。例えばスーパーのレジで、複数の店員さんが同時にお客さんをさばいてるシーンを想像してみて。もし順番をちゃんと管理しなかったら、レジを通った商品より多くの商品が店から出ていっちゃうかも。でもSERIALIZABLEなら、そんなことは絶対に起きない。

SERIALIZABLEレベルの設定例

SERIALIZABLEアイソレーションレベルを設定するには、こんなコマンドを使うよ:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

例えば、このレベルを使ったトランザクションを作ってみよう:

BEGIN; -- トランザクション開始
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- アイソレーションレベルを設定
SELECT * FROM products WHERE category = 'Electronics'; -- 商品リストを取得
UPDATE products SET stock = stock - 1 WHERE product_id = 123; -- 在庫を更新
COMMIT; -- 変更を確定

ケース:映画館のチケット予約

ここで、SERIALIZABLEが本当に必要なリアルな例を見てみよう。君が映画館のオンラインチケット予約システムを作ってるとする。ユーザーが席を選ぶとき、同じ席が同時に2人に売られないように絶対保証したいよね。

まずは席のテーブルを作る:

CREATE TABLE seats (
    seat_id SERIAL PRIMARY KEY,
    is_booked BOOLEAN DEFAULT FALSE
);

次に、いくつか席を追加しよう:

INSERT INTO seats (is_booked) VALUES (FALSE), (FALSE), (FALSE);

SERIALIZABLEでのトランザクション例を見てみよう。

安全に席を予約するにはこんな感じ:

BEGIN; -- トランザクション開始
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- アイソレーションレベルSERIALIZABLE

-- 席が空いてるかチェック
SELECT is_booked FROM seats WHERE seat_id = 1;

-- 席を予約
UPDATE seats SET is_booked = TRUE WHERE seat_id = 1;

COMMIT; -- 予約を確定

もし同じ席を別の並行トランザクションが予約しようとしたら、PostgreSQLは絶対に混乱させず、シリアライズの衝突エラーを投げてくれるよ。

Phantom Readの防止

じゃあ、みんなが嫌がる「ファントムリード」についても見てみよう。Phantom Readは、トランザクションの途中で他のトランザクションがデータを追加・削除して、結果が変わっちゃう現象。たとえば、君のトランザクションが特定の行数を期待してたのに、途中で他のトランザクションが行を追加・削除して結果が変わる、みたいな。

例を見てみよう:

トランザクション開始前のデータ

id balance user
1 1000 アリス
2 500 ボブ

トランザクション1

BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- 残高400より多いユーザーをカウント
SELECT COUNT(*) FROM accounts WHERE balance > 400;

-- 期待する結果:2(アリスとボブ)

トランザクション2

別セッションで並行トランザクションが実行される:

BEGIN;
INSERT INTO accounts (id, balance, user) VALUES (3, 700, 'チャーリー');
COMMIT;

トランザクション1に戻る

-- クエリを再実行
SELECT COUNT(*) FROM accounts WHERE balance > 400;

SERIALIZABLEを使わないと、結果は2じゃなくて3になる。なぜなら、トランザクション1の途中でチャーリーが追加されたから。これがPhantom Readだよ。

でもSERIALIZABLEなら、PostgreSQLはトランザクション1の「世界の見え方」を開始時点で凍結してくれるから、チャーリーは見えない。

SERIALIZABLEレベルの特徴と制約

SERIALIZABLEが理想的な分離を実現してくれるのは分かったよね。でも、世の中に完璧なものなんてない。正直にデメリットも話そう。

パフォーマンス低下
SERIALIZABLEはREAD COMMITTEDやREPEATABLE READよりも、かなりリソースを食う。なぜかというと、PostgreSQLは全部の操作をシリアル実行っぽくエミュレートして、トランザクション間の衝突を全部監視しなきゃいけないから。

シリアライズエラー
もしPostgreSQLが「理想的な順番」でトランザクションを実行できないと判断したら、シリアライズエラー(serialization_failure)を投げてトランザクションをロールバックする。

エラー例:

ERROR: could not serialize access due to concurrent update

こういうときは、失敗したらトランザクションをリトライするのが定番:

DO $$
DECLARE
    done BOOLEAN := FALSE;
BEGIN
    WHILE NOT done LOOP
        BEGIN
            -- トランザクション開始
            BEGIN;
            SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

            -- 操作を実行
            UPDATE accounts SET balance = balance - 100 WHERE id = 1;

            -- 変更を確定
            COMMIT;
            done := TRUE; -- 成功したらループ終了
        EXCEPTION WHEN serialization_failure THEN
            ROLLBACK; -- エラー時はロールバック
        END;
    END LOOP;
END;
$$;

SERIALIZABLEを使うシステムでは、こういう書き方がよくあるよ。

重要!

このコードはPL-SQLで書いてあるよ。PL-SQLについてはまた後でやるから、今は「動くきれいなサンプル」として見ておいて!あと、PL-SQLがなんで必要かも分かるでしょ :)

SERIALIZABLEはいつ使う?

このアイソレーションレベルを使うのが正解なのは、ミスの代償がめちゃくちゃ高いとき:

  • 決済やボーナス配布みたいな金融トランザクション。
  • 在庫管理システムで、注文の重複を絶対に防ぎたいとき。
  • リソース予約の競合を絶対に避けたいオンライン予約システム。

もし君が「データの一貫性100%最優先!パフォーマンスは二の次!」っていうシステムを作ってるなら、SERIALIZABLEは最高の相棒になるよ。

2
タスク
SQL SELF, レベル 40, レッスン 2
ロック未解除
二重予約の防止
二重予約の防止
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION