CQRS(Command Query Responsibility Segregation)
書き込みと読み取りを分離する
CQRS は「書き込み(Command)」と「読み取り(Query)」で異なるモデル・ストレージを使うアーキテクチャパターンです。
なぜ分離するのか
書き込みと読み取りでは、データに求める性質が異なります。
┌─────────────────────────────────────────────────────────────┐
│ 書き込みと読み取りの要件の違い │
├────────────────────┬────────────────────────────────────────┤
│ 書き込み │ 読み取り │
├────────────────────┼────────────────────────────────────────┤
│ 1件ずつ正確に │ 大量データをまとめて集計 │
│ トランザクション保証│ 速度優先(多 少の遅延OK) │
│ 整合性が最重要 │ 柔軟なクエリ(アドホック分析) │
│ 正規化されたモデル │ 非正規化・事前集計されたモデル │
│ 行指向が適切 │ 列指向が適切 │
│ │ │
│ → PostgreSQL │ → ClickHouse, Elasticsearch 等 │
└────────────────────┴────────────────────────────────────────┘
1つのDBで両方を賄おうとすると、どちらかが犠牲になる。
→ 分ければ、それぞれ最適なストレージ・モデルを使える。
アーキテクチャ
従来(単一モデル)
┌─────────────────────────────────────────────┐
│ アプリケーション │
│ │
│ 書き込み ──┐ │
│ ├──→ 同じDB、同じテーブル │
│ 読み 取り ──┘ 同じモデル │
│ │
│ ┌───────────────────────────────────────┐ │
│ │ PostgreSQL │ │
│ │ events テーブル │ │
│ │ ├── INSERT (書き込み) │ │
│ │ └── SELECT COUNT GROUP BY (読み取り) │ │
│ │ → 数億行になると遅い │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
CQRS(分離モデル)
┌─────────────────────────────────────────────────────────────┐
│ アプリケーション │
│ │
│ ┌─ Command 側 ─────────────────────────────────────────┐ │
│ │ │ │
│ │ 「書く」ための最適なモデル │ │
│ │ ┌───────────────────────────────┐ │ │
│ │ │ PostgreSQL │ │ │
│ │ │ ・INSERT events │ │ │
│ │ │ ・トランザクション保証 │ │ │
│ │ │ ・正規化されたスキーマ │ │ │
│ │ └──────────────┬────────────────┘ │ │
│ │ │ │ │
│ └──────────────────┼────────────────────────────────────┘ │
│ │ イベント伝搬 │
│ │ (CDC / Message Queue / Pub-Sub) │
│ │ │
│ ┌─ Query 側 ────────┼──────────────────────────────────┐ │
│ │ ▼ │ │
│ │ 「読む」ための最適なモデル │ │
│ │ ┌───────────────────────────────┐ │ │
│ │ │ ClickHouse │ │ │
│ │ │ ・COUNT / GROUP BY が爆速 │ │ │
│ │ │ ・列指向、圧縮 │ │ │
│ │ │ ・非正規化OK │ │ │
│ │ └───────────────────────────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────────────────┘
イベント伝搬の方法
Command 側で書いたデータを Query 側に届ける方法。
┌─────────────────────────────────────────────────────────────┐
│ イベント伝搬の選択肢 │
│ │
│ ① CDC(Change Data Capture) │
│ ┌──────────┐ WAL ┌────────┐ ┌────────────┐ │
│ │PostgreSQL│ ────→ │ PeerDB │ ────→ │ ClickHouse │ │
│ └──────────┘ └────────┘ └────────────┘ │
│ ・アプリ変更不要 │
│ ・DBレベルで完結 │
│ ・遅延: 数秒 │
│ │
│ ② メッセージキュ ー │
│ ┌──────┐ ┌───────┐ ┌────────────┐ │
│ │ App │ → │ Kafka │ → │ ClickHouse │ │
│ └──────┘ └───────┘ └────────────┘ │
│ ・アプリがイベントを明示的に publish │
│ ・複数の購読者に同時配信可能 │
│ ・遅延: ミリ秒〜秒 │
│ │
│ ③ アプリ内 Pub/Sub │
│ ┌──────────────────────────────────────┐ │
│ │ App │ │
│ │ EventPublisher → EventListener │ │
│ │ ├→ 統計更新 │ │
│ │ ├→ 通知送信 │ │
│ │ └→ 監査ログ │ │
│ └──────────────────────────────────────┘ │
│ ・最もシンプル(Spring の @EventListener 等) │
│ ・同一プロセス内 │
│ ・遅延: なし │
│ │
└─────────────────────────────────────────────────────────────┘
結果整合性(Eventual Consistency)
CQRS では Command 側と Query 側のデータに遅延が生まれます。
時刻 0:00 ユーザーがログイン
時刻 0:00 PostgreSQL に INSERT(Command 側は即座に反映)
時刻 0:03 CDC で ClickHouse に到達(Query 側は3秒遅延)
→ 0:00〜0:03 の間、Command 側と Query 側で状態が異なる
→ これが「結果整合性」