Потоковая обработка
CQRS
LinkedIn в 2011 году имел одну базу данных для всего: профили, связи, сообщения, поиск. Страница профиля требовала 50+ JOIN запросов. При пике нагрузки база падала под тяжестью чтений, хотя записей было в 20 раз меньше. Решение: разделить модели. Профиль писался в нормализованную реляционную БД. Читался из денормализованного документа в Voldemort (key-value store). CQRS в промышленном масштабе - это то, что позволило LinkedIn вырасти до 200 млн пользователей.
- **Event Sourcing + CQRS:** Microsoft Azure, Netflix используют CQRS в связке с Event Sourcing; write side хранит только события, read side строит проекции
- **Elasticsearch как read model:** многие системы пишут в PostgreSQL (write side) и индексируют в Elasticsearch (read side) для полнотекстового поиска - классический CQRS паттерн
- **Axon Framework (Java):** dedicated CQRS/Event Sourcing фреймворк, используемый в banking и insurance системах где аудит и история критичны
Разделение чтения и записи
В большинстве приложений чтений в 10-100 раз больше, чем записей. При этом требования к модели данных для чтения (агрегация, денормализация, проекции) противоречат требованиям для записи (нормализация, консистентность, бизнес-правила). CQRS разрешает это противоречие радикально: две разные модели для двух разных операций. Command изменяет состояние. Query возвращает данные. Никогда вместе.
CQRS не требует отдельных баз данных - это спектр. Простейший уровень: разные методы в одном сервисе с разными моделями. Средний: отдельные read и write сервисы, одна БД. Полный: отдельные хранилища, асинхронная синхронизация. Greg Young (автор термина): начинать с простого, применять CQRS там, где есть реальная проблема асимметрии нагрузки.
Какова главная причина разделения моделей чтения и записи в CQRS?
Eventual Consistency в CQRS
Самое неудобное следствие CQRS с отдельными хранилищами - eventual consistency. Запись прошла в write-store, но read-store обновится через 50-500 миллисекунд. Пользователь создал заказ, нажал 'Мои заказы' - и не видит его. Это не баг - это архитектурное решение с ценой. Вопрос 'как долго стаял рассогласованным?' определяет приемлемость CQRS для конкретного домена.
Паттерны для управления eventual consistency: Read-your-writes (после команды читать из write-store некоторое время), Optimistic UI (показывать результат немедленно, откатить если синхронизация провалилась), версионирование (клиент передает версию, ждет пока read-store достигнет этой версии). Consistency window: обычно 100-500ms при Kafka, 10-50ms при Redis pub/sub.
Пользователь создал пост в социальной сети и сразу перешел на список постов - пост не появился. Через 2 секунды обновил - пост появился. Какой паттерн решил бы это UX-проблему?
Materialized Views как Read Model
Read-модель в CQRS - это, по сути, кеш, синхронизируемый событиями. Materialized view - физическое воплощение этой идеи в базе данных: предвычисленный результат запроса, хранящийся как таблица. Вместо JOIN по 5 таблицам при каждом запросе - единственный SELECT из денормализованной проекции. Обновление происходит через события или триггеры - не при каждом чтении.
PostgreSQL MATERIALIZED VIEW: CREATE MATERIALIZED VIEW ... AS SELECT ... WITH DATA; REFRESH MATERIALIZED VIEW CONCURRENTLY - без блокировки. Redis как read model: HSET order:summary:{id} status pending total 9999. Event-driven update: Kafka consumer слушает события и обновляет read store. Elasticsearch как read store: полнотекстовый поиск по денормализованным документам.
В CQRS с event-driven read model: что происходит если Kafka consumer падает на 5 минут и затем восстанавливается?
Синхронизация и согласованность
Самая сложная часть CQRS - не архитектура, а операционный вопрос: что делать когда read model рассинхронизировалась? Частичный сбой consumer, схема изменилась, событие пришло out-of-order. Ответ: идемпотентность и возможность воспроизведения. Если каждый projector идемпотентен (обработка одного события дважды = обработка один раз), то любую read model можно пересоздать заново из event log.
Snapshot pattern: периодическая материализация текущего состояния + события после snapshot, чтобы не replay с начала истории. Idempotency key: уникальный ключ события для предотвращения дублирования. At-least-once delivery + idempotent consumer = exactly-once semantics на уровне бизнес-результата. Dead Letter Queue (DLQ): события, которые не удалось обработать, отправляются в DLQ для ручного разбора.
CQRS нужно применять во всех проектах для масштабируемости
CQRS решает конкретную проблему асимметрии нагрузки и сложности модели; в большинстве CRUD-приложений это оверинжиниринг
CQRS добавляет eventual consistency, сложность синхронизации и операционную нагрузку. Greg Young, автор CQRS, явно предупреждал: применяйте только там, где есть реальная необходимость - высокая нагрузка на чтение, принципиально разные модели для read/write, или Event Sourcing как обязательное требование.
Projector получил одно и то же событие дважды из Kafka (at-least-once delivery). Как идемпотентность предотвращает проблему?
Ключевые идеи
- **Разделение моделей** - write model нормализована для бизнес-правил, read model денормализована для производительности; это осознанный трейдоф
- **Eventual consistency** - неизбежное следствие; Read-your-writes и Optimistic UI скрывают её от пользователя
- **Идемпотентность projector** - ключ к надежности; любую read model можно пересоздать replay событий из Kafka
- **Применять избирательно** - CQRS оправдан при реальной асимметрии нагрузки; для CRUD - это оверинжиниринг
Связанные темы
CQRS тесно связан с паттернами распределенных систем:
- Saga Pattern — Saga управляет распределенными транзакциями; CQRS определяет как разделить команды и запросы внутри каждого шага
- Event Streaming (Kafka) — Kafka - транспорт для синхронизации write и read моделей в CQRS
Вопросы для размышления
- В каких доменах eventual consistency неприемлема (банковские транзакции, медицинские записи)? Как там применяется CQRS, если вообще применяется?
- Если read model устарела и содержит неверные данные, как организовать горячий rebuild без простоя сервиса?
- CQRS часто упоминают вместе с Event Sourcing. Можно ли применять их независимо друг от друга? В чём разница?