第24章:順序問題② 因果(ざっくり)とバージョン付け🧵🕰️
この章で身につけること🎯✨
- 「どっちが先?」を最低限のルールで判断できるようになる🧠✅
- version番号で「古い更新」を弾けるようになる🧱🛡️
- 途中が抜けた(順番が飛んだ)ときに「再取得(リシンク)」へ逃がせるようになる🚪📥
1) まず1分で体感:順番がズレると何が壊れる?😵💫📨🔀
たとえば「注文ステータス」がこう変わるとするね👇
- v1:
PENDING(受付) - v2:
PAID(支払い完了) - v3:
CANCELLED(キャンセル)
本当は「最後はCANCELLED」になってほしいのに… メッセージが逆順で届くとこうなる😱
- v3(CANCELLED)が先に届く → 状態はCANCELLED ✅
- 後から v2(PAID)が届く → 状態がPAIDに戻っちゃう ❌💥
これ、分散だと普通に起きるよ(遅延・再送・シャッフルの合わせ技)🌀
2) 因果(ざっくり)ってなに?🧵👀
「因果」って難しく聞こえるけど、超ざっくり言うと👇
“BはAの後に起きた” を言える関係が「因果」だよ🧵✨
分散の世界では、事件(イベント)の順番は全部が一直線じゃない。 「先・後」が言えるものもあれば、言えないものもある(部分順序)📎 この考え方は Lamport の “happened-before” として有名だよ📚🕰️ (lamport.azurewebsites.net)
ここでは数学はやらない!代わりに実務の必殺技👇
3) 物理時計(時刻)より、versionで順番を作る⏰❌➡️🔢✅
時刻(timestamp)で「新しい方を採用!」ってやりたくなるけど… 分散だと時計ズレがあるから危ない⚠️(A機は未来、B機は過去…みたいになる)
だからこの章では、いちばん扱いやすい👇
✅ version番号(単調増加)で順番を作る
- 更新のたびに
version += 1する🔢✨ - 受け取った更新に version を付ける📨
- 今の状態のversionと比較して「古いなら捨てる」🗑️
4) バージョン付けの基本ルール3つ🧠📏
ここだけ覚えると強いよ💪✨
- 古い更新は無視(stale)🧊
- ちょうど次(+1)なら適用✅
- 飛んでたら再取得(gap)📥🧯
つまり👇
ev.version <= current.version→ 古いので捨てる🗑️ev.version === current.version + 1→ 順番通りなので適用✅ev.version > current.version + 1→ 途中が抜けたので再取得📥

5) ハンズオン:versionが古い更新を弾く🧪🧰✨
5-1. 型を用意する🧾✨
// apps/shared/order.ts
export type OrderStatus = "PENDING" | "PAID" | "CANCELLED";
export type Order = {
id: string;
status: OrderStatus;
version: number; // 🔢 これが主役!
};
export type OrderStatusChanged = {
type: "OrderStatusChanged";
orderId: string;
to: OrderStatus;
version: number; // 🔢 「この更新が何番目か」
causedBy: string; // 🧵 相関IDっぽいもの(ログ追跡用)
};
5-2. 適用関数(超重要)を書く🧠🛡️
// apps/worker/applyEvent.ts
import type { Order, OrderStatusChanged } from "../shared/order";
export type ApplyResult =
| { kind: "applied"; order: Order }
| { kind: "stale"; reason: string }
| { kind: "gap"; reason: string };
export function applyOrderEvent(order: Order, ev: OrderStatusChanged): ApplyResult {
// ① 古い更新は捨てる🧊
if (ev.version <= order.version) {
return {
kind: "stale",
reason: `stale event. current=v${order.version}, incoming=v${ev.version}`,
};
}
// ② ちょうど次ならOK✅
if (ev.version === order.version + 1) {
return {
kind: "applied",
order: { ...order, status: ev.to, version: ev.version },
};
}
// ③ 飛んでたら再取得📥
return {
kind: "gap",
reason: `gap detected. current=v${order.version}, incoming=v${ev.version}`,
};
}
5-3. 逆順でイベントを流して壊してみる😈➡️🧯
// apps/worker/simulate.ts
import { applyOrderEvent } from "./applyEvent";
import type { Order, OrderStatusChanged } from "../shared/order";
const order: Order = { id: "o-1", status: "PENDING", version: 1 };
const ev2: OrderStatusChanged = {
type: "OrderStatusChanged",
orderId: "o-1",
to: "PAID",
version: 2,
causedBy: "req-aaa",
};
const ev3: OrderStatusChanged = {
type: "OrderStatusChanged",
orderId: "o-1",
to: "CANCELLED",
version: 3,
causedBy: "req-bbb",
};
// 😈 逆順で来た体で適用してみる
let current = order;
for (const ev of [ev3, ev2]) {
const r = applyOrderEvent(current, ev);
console.log("incoming", ev.to, "v" + ev.version, "=>", r.kind, r.reason ?? "");
if (r.kind === "applied") current = r.order;
// gapなら「再取得」へ(ここではログだけ)
if (r.kind === "gap") {
console.log("🔁 need resync! fetching latest order...");
}
}
console.log("final:", current);
期待する観察ポイント👀✨
- v3が来た時点で
gapになって「再取得しよ〜」になる📥 - v2が後から来ても
appliedされて、順番が整う✅ - もし “再取得” を本当に実装したら、v3も最終的に取り込める🎯
6) 「再取得(リシンク)」ってどうやるの?📥🔁
gap を見つけたら、いったんこうするのが現実的👇
- GETで最新の状態を取り直す(例:
GET /orders/o-1)📡 - 取り直した
versionを基準にして、またイベントを適用する🔄
ポイントはこれ👇 「抜けたイベントを気合で当てる」より「最新状態を取り直す」方が安全🧯✨
7) もう1つの定番:HTTPでもversionチェックできる(If-Match / ETag)🏷️🛡️
APIで「編集の競合」を防ぐなら、HTTPの条件付きリクエストが便利だよ📨✨
If-Match は ETag が一致するときだけ更新し、ズレたら 412 Precondition Failed を返すのが基本🧠✅ (MDN Web Docs)
例:注文を更新するときにIf-Matchを使う🧾✨
- サーバーが
ETag: "order-o-1-v3"を返す🏷️ - クライアントは更新時に
If-Match: "order-o-1-v3"を付ける📨 - サーバー側で一致しなければ
412で拒否🛑
これ、考え方としては「version一致したら更新OK」ってことだよ🔢✅
8) TypeScriptまわりの“最新”ミニメモ🧠✨
- TypeScript 5.9 は公式にリリース告知が出ていて、公式ドキュメントにも 5.9 のリリースノートがあるよ📚✨ (Microsoft for Developers)
- Node.js は v24 が Active LTS、v25 が Current としてリストに載ってる(2026-01時点)🟢🟡 (Node.js)
(教材コードは “特殊な新機能” に寄せず、どの環境でも通用する「versionで守る設計」に寄せるのが安全だよ🛡️✨)
9) よくある落とし穴(ここで事故が減る)⚠️🧯
- versionの更新は原子的に(DB更新は「version一致で更新」みたいにする)🔒
- versionは“対象ごと”に持つ(注文ごと、在庫アイテムごと)🧩
- timestampの大小でLWWに逃げない(時計ズレで負ける)⏰💥
gapを無視しない(抜けたまま進むと、あとで必ず変な状態になる)🌀😵💫
10) ミニ演習✍️🧪✨
演習1:説明してみよう🗣️💡
- 「v3が先に来て、v2が後に来る」と何が危ない?
- versionチェックがあるとどう防げる?
演習2:保留キューを作る📦🧵
gap のとき、イベントを捨てずに一旦 pending[orderId] に入れて、
再取得後に「入ってる分を順番に適用」してみよう🔄✨
演習3:If-Matchっぽい更新をAPIに入れる🏷️
GET /orders/:idで ETag を返すPUT /orders/:idはIf-Matchが一致したら更新、違えば412
11) AI活用コーナー🤖💬✨
そのまま貼って使えるプロンプト例👇
- 「OrderStatusChangedイベントが逆順・重複で届くケースのテストを10個作って。期待結果も書いて」🧪🤖
- 「applyOrderEventの分岐(stale/gap/applied)で、ログに出すべき項目を提案して」🪵🤖
- 「If-Match/ETagで412を返す最小のExpress実装をTypeScriptで」🏷️🤖
まとめ(結論1行)✍️✨
順番がズレる世界では、“時刻”より“version”で新しさを判断して、古い更新は弾き、飛びが出たら再取得で守る🛡️🔢📥