data-scale · Stage 3
分離異常を再現し、abortとretryまで設計する
決定的schedulerでnon-repeatable readとwrite skewを再現し、Serializableのabort後にトランザクション全体をretryして業務不変条件を回復する。
到達目標
同じ並行scheduleからRead Committedのnon-repeatable readとSnapshot Isolationのwrite skewをevent traceで再現できる
- 固定schedule、event trace、二つの分離異常、serialization failure、whole-transaction retryを含む決定的トランザクション実験
- ACIDのconsistencyが業務ルールを自動推論せず、MVCCまたはSnapshot IsolationがSerializableと同義でない理由を説明する6分間の解説
Serializableで競合する一方をabortし、トランザクション全体のretry後に明示した業務不変条件が回復したことを検証できる
- 固定schedule、event trace、二つの分離異常、serialization failure、whole-transaction retryを含む決定的トランザクション実験
- 不変条件、読み書き集合、event trace、vendor仕様から分離レベルとretry境界を選ぶ判断記録
ACID、MVCC、Snapshot Isolation、SQL分離レベルの保証範囲とvendor差を区別し、並行性変更へ判断を移せる
- 不変条件、読み書き集合、event trace、vendor仕様から分離レベルとretry境界を選ぶ判断記録
- 同時購入者数が増えた在庫引当について分離レベル、競合処理、retry境界を再評価した報告
能力の進行
recognize
atomicity、consistency、isolation、durabilityと、dirty read、non-repeatable read、phantom、write skewを区別できる
証拠: 固定schedule、event trace、二つの分離異常、serialization failure、whole-transaction retryを含む決定的トランザクション実験
explain
MVCCが読取り待ちを減らしても直列化可能性を自動的に保証せず、業務不変条件をdatabaseが推論しない理由を説明できる
証拠: ACIDのconsistencyが業務ルールを自動推論せず、MVCCまたはSnapshot IsolationがSerializableと同義でない理由を説明する6分間の解説
apply
固定scheduleで二つの分離異常を再現し、Serializableのabortをトランザクション境界からretryできる
証拠: 固定schedule、event trace、二つの分離異常、serialization failure、whole-transaction retryを含む決定的トランザクション実験
diagnose
event trace、snapshot、読み書き集合から異常を分類し、単なるlost updateや実装bugという誤診を反証できる
証拠: 不変条件、読み書き集合、event trace、vendor仕様から分離レベルとretry境界を選ぶ判断記録
lead
不変条件、競合率、latency、vendor保証、retry副作用をレビューし、運用可能な分離方針を合意できる
証拠: 同時購入者数が増えた在庫引当について分離レベル、競合処理、retry境界を再評価した報告
なぜ重要か
transactionは複数の操作を一つの成功または失敗へまとめるが、並行実行時にどの履歴まで許すかは分離レベルで変わる。単体testで正しい在庫引当や当直割当も、二つのtransactionが同じ古い状態を根拠に判断すると業務不変条件を壊し得る。
設計対象は分離レベル名だけではない。不変条件を明示し、異常を起こすscheduleを再現し、abortを正常な競合解決として扱い、外部副作用を含めたトランザクション全体のretry境界を決める。
メンタルモデル
transactionを、開始時点の観測、読み集合、判断、書き集合、commitまたはabortからなる履歴として見る。最終値だけでなく、どの値を根拠に各書込みを決めたかをevent traceへ残す。
| 選択 | このlessonでの観測 | 守れること | 残る責任 |
|---|---|---|---|
| Read Committed | 二回のreadが100、80 | 未commit値を読まない | 再読値の変化を許せる処理に限定する |
| PostgreSQL Repeatable Read | 同じsnapshotから別rowを更新 | transaction内のsnapshotを安定させる | write skewで壊れる複数row不変条件を検証する |
| Serializable | 一方をabortし全体retry | 成功した履歴を直列順として説明できる | serialization failure、backoff、上限、外部副作用を扱う |
同じsnapshotを読んだT1とT2のwrite skewを、どの依存検証とretryが防ぐか。
- 不変条件
rowとは別に業務不変条件を固定する。
- 不変条件
「aliceまたはbobの少なくとも一人が当直」をdatabaseのrowとは別に明示する。
順序: 0
- 不変条件
- 並行schedule
二つのtransactionのreadと局所判断を並べる。
- snapshot
T1とT2が同じ開始状態 {alice: on, bob: on} を読む。
順序: 1
- 局所判断
T1はbobが当直なのでaliceを外し、T2はaliceが当直なのでbobを外す。
順序: 2
- snapshot
- 検証と回復
commit可否を判断し、abort後は読み直す。
- 検証
Snapshot Isolationでは別rowへのwriteが双方commitし得る。Serializableでは危険な依存を検出して一方をabortする。
順序: 3
- retry
abortされたT2は古い判断を再利用せず、transaction開始から読み直す。aliceが外れているためbobを当直に残す。
順序: 4
- 検証
業務不変条件、並行read、局所判断、Serializableのabort、transaction全体の再読込を順に説明できる。
| パラメータ | 選択肢 | 既定値 |
|---|---|---|
| 分離とcommit判定 | Snapshot Isolation、Serializable + whole-transaction retry | snapshot |
- 同じsnapshotを読む: T1とT2が開始状態 {alice: on, bob: on} を読み、各transactionが相手を根拠に局所判断する。; 条件 常時; node
snapshot; edge なし - 別rowを更新: Snapshot IsolationではT1がalice、T2がbobを外す。read集合を根拠に別rowへwriteする。; 条件
isolation=snapshot; nodelocal-decision; edge なし - write skew成立: Snapshot Isolationで2件ともcommitし、当直者0人となって業務不変条件に違反する。; 条件
isolation=snapshot; nodevalidation; edge なし - 危険な依存を検証: Serializableは同じreadと局所判断から生じる危険な依存をcommit前に検出する。; 条件
isolation=serializable; nodevalidation; edge なし - T2 attempt 1をabort: T1をcommitし、競合するT2の1件をabortする。abortは正常な競合解決として記録する。; 条件
isolation=serializable; nodevalidation; edge なし - transaction全体をretry: T2 attempt 2は開始から読み直す。合計3 attempt後、bobを残して当直者1人となる。; 条件
isolation=serializable; noderetry; edge なし
| イベント | 開始 | 終了 | 条件 |
|---|---|---|---|
| parameter-change | concurrent-read | concurrent-read | isolation=snapshot |
| next | concurrent-read | snapshot-local-decision | isolation=snapshot |
| timer | concurrent-read | snapshot-local-decision | isolation=snapshot |
| previous | snapshot-local-decision | concurrent-read | isolation=snapshot |
| reset | snapshot-local-decision | concurrent-read | isolation=snapshot |
| next | snapshot-local-decision | write-skew | isolation=snapshot |
| timer | snapshot-local-decision | write-skew | isolation=snapshot |
| previous | write-skew | snapshot-local-decision | isolation=snapshot |
| reset | write-skew | concurrent-read | isolation=snapshot |
| parameter-change | concurrent-read | concurrent-read | isolation=serializable |
| next | concurrent-read | serializable-validation | isolation=serializable |
| timer | concurrent-read | serializable-validation | isolation=serializable |
| previous | serializable-validation | concurrent-read | isolation=serializable |
| reset | serializable-validation | concurrent-read | isolation=serializable |
| next | serializable-validation | transaction-aborted | isolation=serializable |
| timer | serializable-validation | transaction-aborted | isolation=serializable |
| previous | transaction-aborted | serializable-validation | isolation=serializable |
| reset | transaction-aborted | concurrent-read | isolation=serializable |
| next | transaction-aborted | transaction-retried | isolation=serializable |
| timer | transaction-aborted | transaction-retried | isolation=serializable |
| previous | transaction-retried | transaction-aborted | isolation=serializable |
| reset | transaction-retried | concurrent-read | isolation=serializable |
| 結果 | 状態 |
|---|---|
| Snapshot Isolation: 2件commit、当直者0人。不変条件違反をschedule全体で説明する。 | write-skew |
| Serializable: 1件をabortしtransaction全体をretry。合計3 attempt、当直者1人。 | transaction-retried |
現在の状態: 同じsnapshotを読む — T1とT2が開始状態 {alice: on, bob: on} を読み、各transactionが相手を根拠に局所判断する。
このモデルは例示的かつ決定的であり、実システムの完全な再現ではありません。
動く例で考える
決定的schedulerで二つの異常とSerializable retryを比較する
- 前提
- これはlesson-definedのpedagogical simulationであり実databaseではない。aliceとbobの少なくとも一人が当直という明示的な業務不変条件を使う。
- 入力
- 残高100、別transactionによる80への更新、当直状態
{alice: true, bob: true}、並行read-first scheduleと直列scheduleを固定する。 - 操作
- Read Committedでread間へcommitを挿入し、Snapshot Isolationで二transactionを同じsnapshotから進める。Serializableでは競合する一方をabortし、トランザクション全体を新しいsnapshotからretryする。
- 観測
- Read Committedの二回の値は100と80、Snapshot Isolationでは2件ともcommitして当直者0人、Serializableでは1件をabortし、合計3 attempt後に当直者1人となる。
- 結論
- 異常は最終値だけでは診断できない。isolation、schedule、snapshot、commit・abort、retry attempt、不変条件を同じ証拠へ結ぶ。
次のPython 3.13 harnessは固定fixtureだけを使う。network、secret、shell、実databaseへアクセスせず、実vendorの性能や完全な実装挙動を測定したとは主張しない。
python3.13 - <<'PY'
import json
HARNESS = "transaction_scheduler_lab_v1"
def read_committed_non_repeatable_read():
value = 100
trace = []
first_read = value
trace.append({"event": "T1-read-1", "value": first_read})
trace.append({"event": "T2-write", "before": value, "after": 80})
value = 80
trace.append({"event": "T2-commit", "value": value})
second_read = value
trace.append({"event": "T1-read-2", "value": second_read})
trace.append({"event": "T1-commit"})
return {
"id": "read-committed-non-repeatable-read",
"isolation": "Read Committed",
"first_read": first_read,
"second_read": second_read,
"anomaly": "non-repeatable-read",
"event_trace": trace,
}
def snapshot_write_skew():
state = {"alice": True, "bob": True}
alice_snapshot = dict(state)
bob_snapshot = dict(state)
trace = [
{"event": "T1-read", "snapshot": alice_snapshot},
{"event": "T2-read", "snapshot": bob_snapshot},
]
if alice_snapshot["bob"]:
state["alice"] = False
trace.append({"event": "T1-write", "doctor": "alice", "on_call": False})
trace.append({"event": "T1-commit"})
if bob_snapshot["alice"]:
state["bob"] = False
trace.append({"event": "T2-write", "doctor": "bob", "on_call": False})
trace.append({"event": "T2-commit"})
return {
"id": "snapshot-write-skew",
"isolation": "PostgreSQL Repeatable Read",
"committed_transactions": 2,
"invariant_after": any(state.values()),
"anomaly": "write-skew",
"final_state": state,
"event_trace": trace,
}
def serializable_with_retry():
state = {"alice": True, "bob": True}
shared_snapshot = dict(state)
attempts = []
trace = [
{"event": "T1-read", "snapshot": shared_snapshot},
{"event": "T2-read", "snapshot": shared_snapshot},
]
state["alice"] = False
attempts.append(
{"transaction": "T1", "attempt": 1, "result": "committed"}
)
trace.extend(
[
{"event": "T1-write", "doctor": "alice", "on_call": False},
{"event": "T1-commit"},
]
)
# Serializableは危険な依存を一方のabortへ変える。この分岐を
# mutationした場合、下の不変条件assertがfalse greenを防ぐ。
isolation = "serializable"
active_conflict = shared_snapshot["alice"] and not state["alice"]
if isolation == "serializable" and active_conflict:
attempts.append(
{
"transaction": "T2",
"attempt": 1,
"result": "aborted",
"reason": "serialization-failure",
}
)
trace.append(
{
"event": "T2-abort",
"reason": "serialization-failure",
}
)
aborted_transactions = 1
else:
state["bob"] = False
attempts.append(
{"transaction": "T2", "attempt": 1, "result": "committed"}
)
trace.append({"event": "T2-commit-with-stale-decision"})
aborted_transactions = 0
# 古いstatementだけでなくtransaction開始から読み直すため、
# retryはaliceのcommitを観測し、bobを当直に残せる。
retry_snapshot = dict(state)
if not retry_snapshot["alice"]:
retry_action = "keep-bob-on-call"
else:
state["bob"] = False
retry_action = "set-bob-off-call"
attempts.append(
{
"transaction": "T2",
"attempt": 2,
"result": "committed",
"action": retry_action,
}
)
trace.extend(
[
{"event": "T2-retry-read", "snapshot": retry_snapshot},
{"event": "T2-retry-commit", "action": retry_action},
]
)
return {
"id": "serializable-retry",
"isolation": "Serializable",
"aborted_transactions": aborted_transactions,
"retry_scope": "whole-transaction",
"invariant_after_retry": any(state.values()),
"final_state": state,
"attempts": attempts,
"event_trace": trace,
}
def compare_schedule_mutation():
# 同じ局所ルールでも、readを先に揃えるか一方をcommit後に読むかで
# outcomeが変わることを計算し、scheduleを証拠の一部にする。
parallel = snapshot_write_skew()
serial_state = {"alice": True, "bob": True}
if serial_state["bob"]:
serial_state["alice"] = False
if serial_state["alice"]:
serial_state["bob"] = False
baseline = (
"invariant-preserved"
if parallel["invariant_after"]
else "write-skew-invariant-violated"
)
mutated = (
"invariant-preserved"
if any(serial_state.values())
else "invariant-violated"
)
return {
"baseline_schedule": [
"T1-read",
"T2-read",
"T1-write-commit",
"T2-write-commit",
],
"mutated_schedule": [
"T1-read-write-commit",
"T2-read-noop-commit",
],
"baseline_outcome": baseline,
"mutated_outcome": mutated,
"detected": baseline != mutated,
}
serializable = serializable_with_retry()
report = {
"simulator": "pedagogical-deterministic-scheduler",
"fixture_metadata": {
"kind": "simulated",
"real_database": False,
"purpose": "reproduce isolation anomalies with a fixed event schedule",
"provenance": "lesson-authored synthetic on-call and balance fixture",
"limitations": [
"does not execute PostgreSQL or measure its runtime",
"models only the dependencies needed for these three scenarios",
"does not estimate contention, latency, or retry saturation",
],
},
"isolation_scope": {
"vendor": "PostgreSQL",
"version": "18",
"vendor_differences": [
"SQL isolation names do not imply identical behavior across vendors",
"PostgreSQL Repeatable Read uses snapshot isolation semantics",
],
},
"scenarios": [
read_committed_non_repeatable_read(),
snapshot_write_skew(),
serializable,
],
"schedule_mutation": compare_schedule_mutation(),
"consistency_scope": "business-invariant-is-explicit-not-inferred-by-ACID",
"mastery_evidence": {
"lab_steps": [
{"step": 1, "evidence": "fixture provenance, limits, and invariant"},
{"step": 2, "evidence": "Read Committed event trace 100 to 80"},
{"step": 3, "evidence": "two commits and failed on-call invariant"},
{"step": 4, "evidence": "abort plus whole-transaction retry trace"},
{"step": 5, "evidence": "schedule and guard mutation detection"},
],
"assessments": [
{"assessment": 1, "evidence": "write-skew dependency diagnosis"},
{"assessment": 2, "evidence": "whole-transaction retry boundary"},
],
"rubric_dimensions": [
"technical-correctness",
"judgment",
"evidence",
"communication",
],
"transfer": {
"task": "同時購入者数が増える在庫引当の並行性変更で分離レベルとretry境界を再評価する",
"changed_assumption": "concurrency",
"evidence": "contention and retry policy reevaluation record",
},
},
"external_network_used": False,
}
assert serializable["aborted_transactions"] == 1
assert len(serializable["attempts"]) == 3
assert serializable["invariant_after_retry"]
assert report["schedule_mutation"]["detected"]
print(json.dumps(report, ensure_ascii=False, sort_keys=True))
PY
トレードオフと失敗モード
強い分離は不変条件の推論を単純にする一方、競合時のabortや待機を増やし得る。弱い分離を選ぶなら、許容する異常、schema制約や明示lockで守る範囲、補償、観測を具体化する。Serializableを選んでもretry loopが無限、即時再試行、外部通知の重複なら利用者成果は守れない。
- 誤診: 二人とも別rowを更新したのでlost updateである。 反証: 同じrowのwrite上書きはなく、各transactionが別rowを書いたまま複数row不変条件を壊すwrite skewである。
- 誤診: transaction内の二回のreadが違うのでcacheのstale readである。 反証: event traceはT1のread間にT2のwriteとcommitを置き、Read Committedが各statement開始時のcommit済み値を読むnon-repeatable readを再現している。
- 誤診: serialization failureはdatabase障害なので同じSQL文だけ再送すればよい。 反証: そのSQLに至るreadと業務判断も古いためrollback後にtransaction全体を新しいsnapshotから再実行する。
実装ではretry回数と時間に上限を置き、jitter付きbackoff、idempotency、transaction外のmessage送信をoutboxなどで分離する。abort率、lock待機、tail latency、retry枯渇を観測し、並行性の仮定が変わったら判断を更新する。
知識チェック
- ACID consistencyが「当直者を一人以上にする」という規則を自動的に守らないのはなぜか。規則をdatabaseが検証できる形へ変える方法も一つ挙げる。
- PostgreSQL 18のRepeatable Readで同じsnapshotを使う二transactionが、なぜ別rowへのwrite skewを起こせるか。read、write、commitの順で説明する。
- serialization failure後のretryで、上限、backoff、外部副作用、利用者への失敗表示をどう設計するか。
合格証拠は異常名の暗記ではなく、固定event traceから不変条件違反を再現し、対照scheduleとretry後の履歴で原因を反証できることである。
出典と次の学習
回復とatomicityの基礎は「Principles of Transaction-Oriented Database Recovery」、分離レベルの曖昧さとSnapshot Isolationは「A Critique of ANSI SQL Isolation Levels」を参照する。対象vendorの現在の意味はPostgreSQL 18の「13.2. Transaction Isolation」、Serializable実装の背景は「Serializable Snapshot Isolation in PostgreSQL」で確認する。
次はcore-13で、単一database transactionの外へ境界を広げ、network partition、重複、順序変更、部分障害のもとでcoordinationと回復を設計する。実productへ適用する前に、使用vendorとversionで同じscheduleの統合testを実行する。
実践ラボ
当直割当の分離異常とSerializable retryを再現する
提出成果物: 二つの分離異常を再現するトランザクション実験
- 二人のうち一人以上が当直であるという業務不変条件と、lesson-authored simulated fixtureの由来・限界を明示する
- Read Committedで同じrowを二回読む間に別transactionをcommitし、値が100から80へ変わるevent traceを保存する
- Snapshot Isolation相当の並行scheduleで二transactionが同じsnapshotから別rowを更新し、双方commit後に当直者が0人になるwrite skewを再現する
- PostgreSQL 18のSerializable相当で一方の競合transactionをabortし、トランザクション全体を新しいsnapshotからretryして不変条件を回復する
- scheduleを直列順へ変えた対照実験とSerializable競合判定mutationを実行し、結果差とmutation検出を証拠へ残す
説明して理解を確かめる
6分で、ACID consistencyが業務ルールを自動推論しない理由、Read CommittedとSnapshot Isolationで起きる異常、Serializableのabortを正常系としてwhole-transaction retryする理由を説明する。
アセスメント
問い: PostgreSQLのRepeatable Readで各transaction内の読取り値は安定しているのに、二人とも当直を外れてしまった。何が起きたか。
期待する証拠: 同じsnapshot、別rowへのwrite、読み書き依存、write skew、不変条件違反、Serializableとの差を示すevent trace
問い: Serializableへ変更したらserialization failureが発生した。失敗したSQL文だけを再実行すればよいか。
期待する証拠: transaction開始後の全readと判断が古い可能性、rollback、backoff、上限、idempotentな外部副作用を含むwhole-transaction retry境界
別問題へ転用する
同時購入者数が増える在庫引当の並行性変更で分離レベルとretry境界を再評価する
復習スケジュール
- 1日後
ACID consistencyと業務不変条件の責任境界を一つの反例で示す
- 7日後
Snapshot Isolationでwrite skewが起きる四つのeventを順に説明する
- 30日後
serialization failure後に一文だけでなくtransaction全体をretryする理由を述べる
- 90日後
ACID consistencyと業務不変条件の責任境界を一つの反例で示す
評価ルーブリック
| 観点 | 未達 | 発展途上 | 熟達 | 卓越 |
|---|---|---|---|---|
| technical-correctness | transactionを使えばすべての並行異常が消えるとみなし、分離レベルとscheduleを示さない | 異常名は挙げるが、snapshot、読み書き集合、commit順、不変条件を対応付けない | non-repeatable readとwrite skewを再現し、Serializable abort後のwhole-transaction retryで不変条件を回復する | predicate依存、retry飽和、外部副作用、vendor差、hotspot設計まで反証条件へ含める |
| judgment | 最強の分離レベルまたはlockを無条件に選び、競合率と失敗処理を考えない | 正しさとthroughputを比較するが、業務不変条件またはretry費用が曖昧である | 不変条件、異常影響、競合率、latency、abort率から分離とretry方針を選ぶ | schema制約、明示lock、直列化、業務補償を組み合わせ、負荷変化で再評価する |
| evidence | 最終値だけを示し、transactionとeventの順序を残さない | event traceはあるが、異常を起こすscheduleまたは対照実験を固定しない | 同じfixtureのschedule、snapshot、commit・abort、attempt、retry結果とmutation検出を保存する | 実vendor統合test、競合率、abort histogram、待機時間をsimulationの限界と分離して追跡する |
| communication | ACIDやMVCCという語だけで業務上の保証を断定する | 分離レベルを示すが、保証対象の不変条件とvendor差を説明しない | 業務不変条件、異常schedule、vendor scope、abort・retry責任、残余riskを関係者へ説明する | 利用者影響と運用指標を用いて、開発・DBA・SRE間の変更判断を主導する |
出典
以下の外部資料は利用者が選択したときだけ開きます。
- Principles of Transaction-Oriented Database Recovery (peer-reviewed)
- A Critique of ANSI SQL Isolation Levels (peer-reviewed)
- 13.2. Transaction Isolation (primary)
- Serializable Snapshot Isolation in PostgreSQL (peer-reviewed)