0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【トランザクションの基本】REPEATABLE READとは?「写真(スナップショット)」で理解する強力な防衛線

0
Posted at

はじめに

PostgreSQLのトランザクション分離レベルにおいて、デフォルトの READ COMMITTED の1つ上に位置するのが「REPEATABLE READ(リピータブルリード)」です。

PostgreSQLの REPEATABLE READ は非常に優秀で、これを設定するだけで「値が変わる(ノンリピータブルリード)」ことも「件数が増える(ファントムリード)」ことも防いでくれます。

なぜそんなに優秀なのか?そして、実務で使う上で絶対に知っておくべき「書く時の厳しいルール(同時更新エラー)」とは何なのか。Docker環境を使ったハンズオンで、その正体を完全に理解しましょう!


1. REPEATABLE READの正体は「自分専用の写真」

PostgreSQLにおける REPEATABLE READ を一言で表すなら、「トランザクション開始時にデータベース全体の『写真』を撮り、終了するまでその写真の世界だけで作業するモード」です。(専門用語でMVCC:多版同時実行制御と呼ばれます)

読むとき(Read)のルール:写真は変わらない

自分が BEGIN を宣言して写真を撮った後、他の人が現実のデータベースをどう書き換えようと、自分の手元にある「写真」は絶対に変化しません。だからこそ、途中でデータが変わることも増えることもなく、安全にデータを読み続けることができます。

書くとき(Write)のルール:写真と現実のズレは許さない

読むだけなら平和ですが、データを「更新(UPDATE)」しようとした瞬間に厳しいルールが発動します。
自分が古い写真を元にデータを上書きしようとした時、もし現実のデータがすでに他人に書き換えられていた場合、PostgreSQLは「現実と写真がズレているから上書きさせない!」と判断し、エラーを出して処理を強制終了させます。(これを「ロストアップデート」の防止と呼びます)

2. シーケンス図で見る:更新衝突(コンフリクト)の瞬間

商品A(1000円)の値段を更新する「Aさん」と「Bさん」のタイムラインです。

もしここでエラーにせず上書きを許してしまうと、Bさんが5000円に変更した事実が「1500円」で上書きされ、完全に消え去ってしまいます。これを防ぐためのエラーなのです。


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

実際にDocker環境で「同時更新エラー」を体験してみましょう。

services:
  db:
    image: postgres:15
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: testdb
    ports:
      - "5432:5432"

コンテナを起動し、データベースに接続します。

docker compose up -d
docker compose exec db psql -U user -d testdb

テスト用の「商品テーブル」を作成します。

CREATE TABLE products (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    price INT
);
INSERT INTO products VALUES (1, '高級キーボード', 10000);

4. 実験:同時更新エラー(シリアライズエラー)を発生させる

ターミナルを2つ(Terminal A, B)開いて実験します。

Step 1: Terminal Aでレベルを上げて確認

-- Terminal A
BEGIN;
-- ★トランザクション分離レベルを厳しくする!
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

SELECT price FROM products WHERE id = 1;
-- 結果: 10000 (★1万円の写真をゲット)

image.png

Step 2: Terminal Bが(裏で)価格を更新して確定

Terminal Aが考えている間に、Terminal Bが価格を「15000円」に値上げしてしまいます。

-- Terminal B
BEGIN;
UPDATE products SET price = 15000 WHERE id = 1;
COMMIT;
-- 現実のデータベースは15000円になりました

image.png

Step 3: Terminal Aが古い写真を元に更新を試みる

TerminalBで更新後、Terminal Aでもう一度状態を確認します。

testdb=*# SELECT price FROM products WHERE id = 1;
 price
-------
 10000
(1 row)

image.png

別で更新がされていても、最初の状態の10000が保たれています。
繰り返し読み返せるから、REPEATABLE READになっています。

次にTerminal Aはまだ手元の写真(10000円)を見ているつもりで、そこから「+2000円」の更新をかけようとします。

-- Terminal A
UPDATE products SET price = price + 2000 WHERE id = 1;

Enterを押した瞬間、以下の強烈なエラーが発生します!

ERROR:  could not serialize access due to concurrent update
(翻訳:同時更新が行われたため、アクセスの直列化に失敗しました)

image.png

この瞬間、Terminal Aのトランザクションは「ABORT(失敗)」状態となり、これ以上何もできなくなります(ROLLBACKするしかなくなります)。


5. まとめ

  • REPEATABLE READ は、開始時に自分専用の写真(スナップショット)を撮るモード。
  • 読むときは、他人の更新に邪魔されず、最初から最後まで一貫したデータを読める。
  • 書くときは、現実のデータが他人に書き換えられていた場合、ロストアップデートを防ぐために必ずエラーになる。
  • エラーが起きることを前提に、アプリケーション側でリトライ処理を実装するのがプロの設計。

単にデータを安全に読みたい時や、「現在の値を読み込んで、複雑な計算をしてから更新する」ような重要処理の際に、ぜひ REPEATABLE READ を活用してみてください!

0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?