/note/tech

CQRS/ESの『整合性どうするの?』に答えてみる

要約:

■ 1. 問題提起と結論

  • CQRS/ESで読み書きDBを分けた場合に古いデータを読む可能性があるという疑問が学習者の間で生じやすい
  • 結論:整合性はコマンド側モデルで守る
  • クエリ側DBは整合性の判断に使用しない

■ 2. よくある誤解と実際の動き

  • よくある誤解:
    • 書き込みDBは書き込み専用
    • 読み取りDBは読み取り専用
    • 両者は完全に分離されている
  • 実際の構成:
    • コマンド側DB(イベントストア):イベントの保存および集約の状態構成
    • クエリ側DB(投影済みビュー):検索・一覧・分析用
  • コマンド実行時の流れ:
    • 対象集約のイベントをイベントストアから読み込む
    • イベントをリプレイして現在のステートを構成する
    • そのステートに対してビジネスルールを適用する
    • 問題なければ新しいイベントを保存する
  • 単一集約の詳細表示画面ではコマンド側のステートを直接取得するのが一般的

■ 3. 整合性を守る2つの仕組み

  • 楽観的バージョンチェック:
    • イベントストアは集約ごとに最新バージョン(イベントの最新ID)を持つ
    • コマンドに参照時点のバージョン(ExpectedVersion)を含める
    • 保存時にExpectedVersionと実際のバージョンが異なれば競合エラーとする
    • ポイント付与のようなケースでは冪等性キー(TransactionId)を使う方法もある
  • 型による状態制約:
    • 状態を型で表現し不正な操作を型レベルで防ぐ
    • 例:AvailableRoomとFullRoomを別の型として定義し満員の部屋には入室メソッド自体が存在しない設計にする
    • 削除フラグのような実行時チェックに依存する必要がなくなる

■ 4. アクターモデルによる同時実行制御

  • 1つの集約に対して1つの実行単位(アクター)を持つ
  • コマンドはメッセージとして順番に処理され1つのコマンドが完了するまで次は実行されない
  • 分散環境でもどのサーバーにリソースがあるかを意識せずに適切なサーバーへルーティングされる

■ 5. パフォーマンス対策

  • アクターによるステートのメモリ保持:
    • コマンド実行後もアクターはしばらくメモリに残る
    • 同じ集約への次のコマンド実行時にメモリ上のステートを再利用しイベントストアへの読み込みを省略できる
  • スナップショットによる高速化:
    • 一定期間アクセスがない場合はアクターをメモリから解放しステートをスナップショットとして保存する
    • 次回呼び出し時はスナップショットから復元しその後に追加されたイベントのみリプレイする
    • アクターモデルとスナップショットの組み合わせによりパフォーマンスの問題は実用上ほとんど発生しない

■ 6. クエリ側DBの役割と使い分け

  • クエリ側DBが使われるケース:
    • 複数集約をまたいだ一覧表示
    • 検索・並び替え
    • 集計・分析
    • レポーティング
  • 一覧画面と詳細画面の使い分け:
    • 一覧画面:クエリ側DB(投影済みビュー)を使用し結果整合性(多少の遅延あり)
    • 詳細画面:コマンド側(イベントストア)を使用し強整合性(常に最新)
  • 「一覧は古いが詳細は正しい」という現象はCQRS/ESの意図された動作であり不具合ではない

■ 7. 用語の整理

  • 「読み込みDB/書き込みDB」という表現は混乱を招きやすい
  • より適切な整理:
    • コマンド側DB(イベントストア):単一集約の状態構成・整合性チェック・ビジネスルールの適用
    • クエリ側DB:複数集約のビュー・検索と一覧と分析・結果整合性を許容する用途
  • 「読み書きDBの完全分離」ではなく「コマンド側とクエリ側の役割分担」として捉えるとCQRS/ESの設計が理解しやすくなる