【DB】トランザクション

データベースの楽観ロックと悲観ロックを理解する

並行制御がないときに起こる問題

🧼 ダーティリード(Dirty Read)=「まだ決まってないのに見ちゃった」

• 🧍‍♂️Aさん:データを変更中(まだ保存=コミットしてない)

• 🧍‍♀️Bさん:その変更中のデータを読んでしまう

🧨 もし Aさんが「やっぱ間違えたわ」ってロールバックすると、

Bさんは「実際には存在しない値を元に処理」してたことになる → 危ない!

🔁 非リピータブルリード(Non-repeatable Read)=「さっきと同じもの読んだのに中身違うやん」

• 🧍‍♂️Aさん:同じデータを2回読みたい

• 🧍‍♀️Bさん:その間にデータを更新してしまう

Aさんは「同じ条件でSELECTしたはず」なのに、

1回目と2回目で値が違ってビックリ → リピートできない=非リピータブル

👻 ファントムリード(Phantom Read)=「おばけ(新しい行)が現れた」

• 🧍‍♂️Aさん:「年齢 > 20 の人一覧」を2回SELECTした

• 🧍‍♀️Bさん:その間に「25歳の人」を新規登録した

→ Aさんは「同じ条件なのに2回目に増えてるやん…」

→ **どこから現れた!?ファントム(幻)!?**ってなる

💥 更新の消失(Lost Update)=「お互いの編集がなかったことに」

• 🧍‍♂️Aさん:商品Aの在庫を10 → 8に更新(読み取り時は10)

• 🧍‍♀️Bさん:同じタイミングで、商品Aの在庫を10 → 7に更新(読み取り時も10)

→ どちらも「10」から更新してる

後から書いた方が先の人の変更を上書きして、8がなかったことになる

📝 まとめ(超ざっくり)

現象何が起きる?イメージ
ダーティリードコミット前の変更を見ちゃう途中の下書きを他人が読んだ
非リピータブルリード同じSELECTなのに結果が変わる2回読んだら中身変わってた
ファントムリード同じ条件でレコード数が増減するさっきいなかったのに現れた
更新の消失どちらかの更新が消えてしまう上書き競争に負けた

非リピータブルリードとファントムリード


この2つ、名前も似てるし、どっちも「SELECTしたら結果変わった」系でごっちゃになるよね。

でも、**違いは「何が変わったか」**に注目するとハッキリするよ👇

🧠 ズバリ違いを一言で

現象名何が変わった?
❌ 非リピータブルリード**既存の行の中身(値)**が変わる
👻 ファントムリード**行の数(存在)**が増減する

👀 具体例で比較

🟡 非リピータブルリード

状況:

• トランザクションAがユーザー「田中」を読み取る → 年齢は 25

• その間に、トランザクションBが「田中の年齢」を 30 に更新&コミット

• Aがもう一度「田中」をSELECTすると → 年齢が 30 になってる

💥 問題点:

同じ田中さんを2回読んだのに、年齢が変わってる!!」


🟣 ファントムリード

状況:

• トランザクションAが「年齢が20以上のユーザー一覧」を読み取る → 田中さんと佐藤さん

• その間に、トランザクションBが「28歳の鈴木さん」を新しくINSERTしてコミット

• Aが再度「年齢が20以上」を読み取る → 鈴木さんが増えてる

💥 問題点:

同じ検索条件で2回読んだのに、レコードが増えてる!!」


🧩 図解イメージ(文字で)

非リピータブルリード:
    読み取り1回目 → ユーザーA:年齢25
    読み取り2回目 → ユーザーA:年齢30 ← 値が変わった(同じ行)

ファントムリード:
    読み取り1回目 → ユーザーA, ユーザーB
    読み取り2回目 → ユーザーA, ユーザーB, ユーザーC ← 行が増えた

📝 まとめ表

項目非リピータブルリードファントムリード
行は同じ?✅ 同じ行を読んでる❌ 別の行が増減する
問題は?値が変わってるレコードが増えた/消えた
対象操作UPDATEINSERT / DELETE

🤝 一言で覚えるなら?

• 🔁 非リピータブル = 「更新された行を読んだら違ってた」

• 👻 ファントム = 「新しく現れた行が見えちゃった」

どっちも「SELECTしたらなんか違ってた」系だけど、

非リピータブル=既存行の変更

ファントム=新しい行の出現

って意識すればバッチリだよ💪✨

「どの分離レベルでどれが防げるか」も気になる?

トランザクションの重要な性質

ACID 特性の解説


トランザクションが信頼性の高いデータ処理を保証するために満たすべき性質として、ACID 特性があります。これは以下の 4 つの頭文字を取ったものです。

原子性(Atomicity): トランザクションに含まれる操作は、全て実行されるか全く実行されないかのどちらかでなければなりません。例えば、銀行振込処理では、引き落としと入金が必ず両方成功するか、両方とも行われないかのどちらかです。中途半端な状態は許されません。

一貫性(Consistency): トランザクションの前後で、データベースは一貫した状態を保たなければなりません。すべての制約、参照整合性、ビジネスルールが満たされている状態です。トランザクション実行中に一時的に不整合が生じても、完了時には整合性が保たれます。

独立性(Isolation): 複数のトランザクションが同時に実行される場合でも、各トランザクションは他のトランザクションの影響を受けずに実行されるように見えなければなりません。これにより、同時実行されるトランザクション間の干渉を防ぎます。

永続性(Durability): 一度コミットされたトランザクションの結果は、システム障害が発生しても失われることなく永続的に保存されなければなりません。これはデータベースがディスクなどの永続メディアに確実に書き込むことで実現されます。

トランザクション分離レベル


並行してトランザクションを実行する際、独立性のレベルを調整できるよう、SQL 標準では 4 つの分離レベルが定義されています。

READ UNCOMMITTED(未コミット読み取り): 最も低い分離レベルです。他のトランザクションがコミットしていない変更も読み取れます。

READ COMMITTED(コミット済み読み取り): 他のトランザクションがコミットした変更のみを読み取れます。

REPEATABLE READ(反復可能読み取り): トランザクション中に同じデータを複数回読み取っても、結果が変わりません。

SERIALIZABLE(直列化可能): 最も厳格な分離レベルです。同時実行されるトランザクションが、順番に一つずつ実行されたかのような結果になります。

分離レベルと発生しうる問題の関係

分離レベルによって、許容される並行処理の問題が異なります。

分離レベルダーティリード非リピータブルリードファントムリード
READ UNCOMMITTED発生する発生する発生する
READ COMMITTED発生しない発生する発生する
REPEATABLE READ発生しない発生しない発生する
SERIALIZABLE発生しない発生しない発生しない

分離レベルを高くするほど、データの整合性は向上しますが、ロックが増えるためパフォーマンスは低下する傾向があります。アプリケーションの要件に応じて適切な分離レベルを選択することが重要です。例えば、高いスループットが必要な読み取り中心のアプリケーションでは READ COMMITTED を、金融取引のような厳密な整合性が必要なアプリケーションでは SERIALIZABLE を選択するといった判断が必要です。

楽観ロック vs 悲観ロック


🧠 ロックの前提思想の違い

種類考え方(メンタルモデル)イメージ
楽観ロック「基本的に競合は起きない」→ あとでチェックする編集後の保存時に「上書きOKか確認」
悲観ロック「どうせ競合が起きる」→ 最初にロックして保護編集前に「予約して他をブロック」

🛠 実装方法の違い

項目楽観ロック悲観ロック
🔧 主な方法– バージョン番号(version)- タイムスタンプ- ハッシュ値- 元の値の比較– SELECT … FOR UPDATE- テーブルロック/行ロック
🔄 データ更新時更新前の状態と一致するか比較してから保存先にロックを取得してから編集開始
💥 競合時例外を投げてアプリ側でリトライ・警告など対応競合は発生しないが、ロック待ちが起こる

✅ 処理の流れ(比較)

🌈 楽観ロック(例:バージョン番号)

1. データを取得(例:version = 5)

2. 編集

3. 保存時に WHERE id = 1 AND version = 5 のように更新を試みる

4. 一致すれば version++ して更新、ズレてたら失敗扱い(=他の誰かが更新済)


🔒 悲観ロック(例:SELECT … FOR UPDATE)

1. SELECT * FROM items WHERE id = 1 FOR UPDATE ← 他が触れないようロック

2. 編集

3. コミット or ロールバックするとロック解放


✅ 向いているケース

シチュエーション向いているロック
読み込みが多く、更新は稀楽観ロック 👍
競合が滅多に起きない楽観ロック 👍
頻繁に同じデータが同時に更新される悲観ロック 👍
絶対に整合性を守らなければいけない(金融など)悲観ロック 👍

🎁 メリット・デメリット比較

項目楽観ロック悲観ロック
✅ メリット– 高い並行性(ロックなし)- スケーラブル- デッドロックなし– データ整合性が強固- 実装が単純なケースもある
❌ デメリット– 実装がやや複雑- 競合時にリトライ処理が必要- レコード単位でしか検出できないことが多い– 同時アクセスに弱い- デッドロックの可能性あり- パフォーマンス低下しやすい

🔍 補足:バージョン番号による楽観ロックの実装イメージ(SQL)

UPDATE users
SET name = '新しい名前', version = version + 1
WHERE id = 1 AND version = 5;

※このクエリで1件も更新されなかったら、「誰かが先に更新した」って判断してリトライやエラーメッセージを出す。


🚀 まとめ(どっちを使うべき?)

条件オススメ
データ競合が「まれ」楽観ロック 👍
データ競合が「よく起きる」悲観ロック 👍
速度・スケーラビリティ重視楽観ロック 👍
データの正確さが最重要(金融等)悲観ロック 👍

もし実際に「Laravelでバージョンカラム使って楽観ロックやりたい」とか「悲観ロックのSQL構文知りたい」とかあれば、具体例出せるよ!

現場の設計に合わせた選定の相談もぜんぜんOK💡

コメント

タイトルとURLをコピーしました