ストーリー
結果整合性(Eventual Consistency)とは
分散システムにおいて、データの変更がすべてのノード/サービスに反映されるまでに時間差がある状態です。「最終的には整合する」ことは保証されますが、その間は古いデータが見える可能性があります。
graph LR
subgraph "強い一貫性(Strong Consistency)"
W1["Write"] --> R1["Read<br/>常に最新値を返す"]
end
subgraph "結果整合性(Eventual Consistency)"
W2["Write"] -.->|"遅延"| R2["Read<br/>一時的に古い値"]
W2 -->|"少し待つ"| R3["Read<br/>最新値を返す"]
end
結果整合性が発生する場面
// 場面1: CQRSのRead Model更新遅延
// 注文を作成してから一覧に反映されるまでのタイムラグ
// 場面2: キャッシュの更新遅延
// DBを更新してからCDNキャッシュが更新されるまでのタイムラグ
// 場面3: サービス間のイベント伝播遅延
// 在庫サービスが在庫を減らしてから、
// 商品サービスの在庫表示が更新されるまでのタイムラグ
// 場面4: レプリケーション遅延
// Primary DBに書き込んでから、
// Replica DBに反映されるまでのタイムラグ
UXを損なわない設計パターン
1. オプティミスティックUI(楽観的UI更新)
// フロントエンド: APIレスポンスを待たずにUIを更新
async function handleCreateOrder(orderData: OrderData): Promise<void> {
// 1. UIを即座に更新(楽観的)
const tempOrder: OrderView = {
id: `temp-${Date.now()}`,
...orderData,
status: "PENDING",
isOptimistic: true, // まだサーバー確認前
};
uiStore.addOrder(tempOrder);
try {
// 2. APIを呼び出し
const result = await orderApi.create(orderData);
// 3. 成功 → 一時データを本物に差し替え
uiStore.replaceOrder(tempOrder.id, {
...result,
isOptimistic: false,
});
} catch (error) {
// 4. 失敗 → 楽観的な更新を取り消し
uiStore.removeOrder(tempOrder.id);
showError("注文の作成に失敗しました");
}
}
2. 確認画面での即座のフィードバック
// コマンド実行後、確認画面に遷移して最新状態を表示
async function submitOrder(data: OrderData): Promise<void> {
// コマンドの結果(Write Modelから)を直接返す
const result = await api.post("/orders", data);
// Read Modelではなく、コマンドの結果を使って確認画面を表示
router.navigate("/orders/confirmation", {
orderId: result.orderId,
status: result.status,
// Read Modelの反映を待つ必要がない
});
}
3. Read-your-writes 整合性
// 書き込み後の読み取りで、自分の書き込みが見えることを保証
class ReadYourWritesProxy {
async query(userId: string, queryFn: () => Promise<OrderReadModel[]>): Promise<OrderReadModel[]> {
const result = await queryFn();
// ユーザーの最近の書き込みと照合
const recentWrites = await this.getRecentWrites(userId);
for (const write of recentWrites) {
const found = result.find(r => r.orderId === write.orderId);
if (!found) {
// Read Modelにまだ反映されていない → 書き込み結果を補完
result.unshift(write.optimisticView);
}
}
return result;
}
}
4. ポーリングとリアルタイム通知
// 注文後、ステータスの変化をリアルタイムで通知
class OrderStatusTracker {
// WebSocketで変更通知を受信
subscribeToOrderUpdates(orderId: string, callback: (status: string) => void): void {
this.websocket.subscribe(`order.${orderId}.status`, (event) => {
callback(event.status);
});
}
// または、ポーリングで確認
async pollUntilReady(orderId: string, maxWait: number = 10000): Promise<OrderReadModel> {
const start = Date.now();
while (Date.now() - start < maxWait) {
const order = await this.readStore.get(orderId);
if (order) return order;
await sleep(500); // 500msごとに確認
}
throw new Error("Read Model update timeout");
}
}
結果整合性の許容範囲
| 場面 | 許容遅延 | 対策 |
|---|---|---|
| 注文確認画面 | 0秒 | コマンドの結果を直接表示 |
| 注文一覧 | 1〜3秒 | オプティミスティックUI |
| 商品検索結果 | 数秒〜分 | キャッシュTTL |
| 分析ダッシュボード | 分〜時間 | バッチ更新 |
| 月次レポート | 日 | バッチ処理 |
結果整合性が許容できない場面
// 結果整合性が許容できない場合の対策
const strictConsistencyCases = {
// 在庫の二重販売防止
inventory: {
problem: "結果整合性だと在庫以上の注文を受け付ける可能性",
solution: "在庫確認はWrite Model(同期)で行う",
},
// 残高チェック
balance: {
problem: "結果整合性だとマイナス残高になる可能性",
solution: "残高確認と引き落としを同一トランザクションで実行",
},
// 一意性制約
uniqueness: {
problem: "ユーザー名の重複登録",
solution: "Write Modelのユニーク制約で保証",
},
};
// ルール: 「ビジネスルールの検証」はWrite Model(強い一貫性)
// 「表示・検索」はRead Model(結果整合性OK)
まとめ
| ポイント | 内容 |
|---|---|
| 結果整合性 | 最終的に整合するが、一時的なズレが発生 |
| UX設計 | オプティミスティックUI、確認画面、Read-your-writes |
| 許容範囲 | 場面に応じて0秒〜日単位まで異なる |
| 使い分け | ビジネスルール検証 = 強い一貫性、表示 = 結果整合性OK |
チェックリスト
- 結果整合性の定義を説明できる
- オプティミスティックUIを理解した
- Read-your-writes整合性の概念を理解した
- 結果整合性が許容できない場面を判断できる
次のステップへ
次は分散システムのテスト戦略を学びます。Contract TestingとChaos Engineeringで、分散システムの信頼性を高める方法を理解しましょう。
推定読了時間: 30分