LESSON 30分

ストーリー

高橋アーキテクト
注文したのに、注文一覧に表示されません!
高橋アーキテクト
これが結果整合性だ。最終的には整合するが、一時的なズレは発生する。大事なのは、このズレをユーザーに意識させない設計をすることだ

結果整合性(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分