GH GambleHub

CQRS и разделение чтения/записи

Что такое CQRS

CQRS (Command Query Responsibility Segregation) — архитектурный подход, который разделяет модель данных и компоненты, отвечающие за запись (commands) и чтение (queries).
Идея: процесс изменения состояния оптимизируется под валидные инварианты и транзакции, а чтение — под быстрые, целевые проекции и масштабирование.

💡 Команды изменяют состояние и возвращают результат операции. Запросы только читают и не имеют побочных эффектов.

Зачем это нужно

Производительность чтения: материализованные проекции под конкретные сценарии (ленты, отчеты, каталоги).
Стабильность критического пути: запись изолирована от «тяжелых» джоинов и агрегатов.
Свобода выбора хранилищ: OLTP для записи, OLAP/кеш/поисковые движки для чтения.
Ускоренная эволюция: добавляйте новые представления без риска «сломать» транзакции.
Наблюдаемость и аудит (особенно в связке с Event Sourcing): легче восстановить и переиграть состояние.


Когда применять (и когда нет)

Подходит, если:
  • Превалируют чтения с разными срезами данных и сложной агрегацией.
  • Критический путь записи должен быть тонким и предсказуемым.
  • Нужны разные SLO/SLA для чтения и записи.
  • Требуется изоляция доменной логики записи от аналитических/поисковых нужд.
Не подходит, если:
  • Домен прост, нагрузка невысока; CRUD справляется.
  • Сильная согласованность между чтением и записью обязательна для всех сценариев.
  • Команда неопытна, а операционная сложность неприемлема.

Базовые понятия

Команда (Command): намерение изменить состояние (`CreateOrder`, `CapturePayment`). Проверяет инварианты.
Запрос (Query): получение данных (`GetOrderById`, `ListUserTransactions`). Без побочных эффектов.
Модель записи: агрегаты/инварианты/транзакции; хранение — реляционное/ключ-значение/событийный лог.
Модель чтения (проекции): материализованные таблицы/индексы/кеш, синхронизируются асинхронно.
Согласованность: часто eventual между записью и чтением; критические пути — через прямое чтение из write-модели.


Архитектура (скелет)

1. Write-сервис: принимает команды, валидирует инварианты, фиксирует изменения (БД или события).
2. Outbox/CDC: гарантированная публикация факта изменений.
3. Процессоры проекций: слушают события/CDC и обновляют read-модели.
4. Read-сервис: обслуживает queries из материализованных представлений/кешей/поиска.
5. Саги/оркестрация: координируют кросс-агрегатные процессы.
6. Наблюдаемость: лаг проекций, процент успешных применений, DLQ.


Проектирование модели записи

Агрегаты: четкие границы транзакций (например, `Order`, `Payment`, `UserBalance`).
Инварианты: формализуйте (денежные суммы ≥ 0, уникальность, лимиты).
Команды идемпотентны по ключу (например, `idempotency_key`).
Транзакции минимальны по охвату; внешние побочные эффекты — через outbox.

Пример команды (псевдо-JSON)

json
{
"command": "CapturePayment",
"payment_id": "pay_123",
"amount": 1000,
"currency": "EUR",
"idempotency_key": "k-789",
"trace_id": "t-abc"
}

Проектирование модели чтения

Отталкивайтесь от запросов: какие экраны/отчеты нужны?
Денормализация допустима: read-модель — «оптимизированный кэш».
Несколько проекций для разных задач: поиск (OpenSearch), отчеты (колонночное хранилище), карточки (KV/Redis).
TTL и пересборка: проекции должны уметь восстанавливаться из источника (реплей событий/снапшоты).


Согласованность и UX

Eventual consistency: интерфейс может кратковременно показывать старые данные.
UX-паттерны: «данные обновляются…», optimistic UI, индикаторы синхронизации, блокировка опасных действий до подтверждения.
Для операций, требующих сильной согласованности (например, показ точного баланса перед списанием), читайте напрямую из write-модели.


CQRS и Event Sourcing (по желанию)

Event Sourcing (ES) хранит события, а состояние агрегата — результат их свертки.
Связка CQRS+ES дает идеальный аудит и легкую пересборку проекций, но повышает сложность.
Альтернатива: обычная OLTP-БД + outbox/CDC → проекции.


Репликация: Outbox и CDC

Outbox (в одной транзакции): запись доменных изменений + запись события в outbox; паблишер доставляет в шину.
CDC: считывание из лога БД (Debezium и т. п.) → трансформация в доменные события.
Гарантии: по умолчанию at-least-once, потребители и проекции должны быть идемпотентны.


Выбор хранилищ

Write: реляционные (PostgreSQL/MySQL) для транзакций; KV/Document — где инварианты простые.

Read:
  • KV/Redis — карточки и быстрые ключевые чтения;
  • Поиск (OpenSearch/Elasticsearch) — поиск/фильтры/фасеты;
  • Колонночные (ClickHouse/BigQuery) — отчеты;
  • Кеш на CDN — публичные каталоги/контент.

Паттерны интеграции

API слой: отдельные эндпоинты/сервисы для `commands` и `queries`.
Идемпотентность: ключ операции в заголовке/теле; хранение recent-keys с TTL.
Саги/оркестрация: тайм-ауты, компенсации, повторяемость шагов.
Backpressure: ограничение параллелизма процессоров проекций.


Наблюдаемость

Метрики write: p95/99 латентности команд, доля успешных транзакций, ошибки валидации.
Метрики read: p95/99 запросов, hit-rate кеша, нагрузка на поисковый кластер.
Лаг проекций (время и сообщения), DLQ-ставка, процент дедупликаций.
Трейсинг: `trace_id` проходит через команду → outbox → проекцию → query.


Безопасность и комплаенс

Разделение прав: разные scopes/роли для записи и чтения; принцип наименьших привилегий.
PII/PCI: минимизируйте в проекциях; шифрование at-rest/in-flight; маскирование.
Аудит: фиксируйте команду, актера, исход, `trace_id`; WORM-архивы для критичных доменов (платежи, KYC).


Тестирование

Contract tests: для команд (ошибки, инварианты) и queries (форматы/фильтры).
Projection tests: подайте серию событий/CDC и проверьте итоговую read-модель.
Chaos/latency: инжекция задержек в процессоры проекций; проверка UX при лаге.
Replayability: пересборка проекций на стенде из снапшотов/лога.


Миграции и эволюция

Новые поля — аддитивно в событии/CDC; read-модели пересобираются.
Двойная запись (dual-write) при редизайне схем; старые проекции держим до переключения.
Версионирование: `v1`/`v2` событий и эндпоинтов, Sunset-план.
Feature flags: ввод новых queries/проекций по канарейке.


Антипаттерны

CQRS «ради моды» в простых CRUD-сервисах.
Жесткая синхронная зависимость чтения от записи (убивает изоляцию и устойчивость).
Один индекс на все: смешивание разнородных запросов в один read-store.
Нет идемпотентности у проекций → дубли и расхождения.
Невосстанавливаемые проекции (нет реплея/снапшотов).


Примеры доменов

Платежи (онлайн-сервис)

Write: `Authorize`, `Capture`, `Refund` в транзакционной БД; outbox публикует `payment.`.

Read:
  • Redis «карточка платежа» для UI;
  • ClickHouse для отчетов;
  • OpenSearch для поиска транзакций.
  • Критический путь: авторизация ≤ 800 мс p95; согласованность чтения для UI — eventual (до 2–3 с).

KYC

Write: команды на старт/апдейт статуса; хранение PII в защищенной БД.
Read: облегченная проекция статусов без PII; PII подтягивается точечно при необходимости.
Безопасность: разные scopes на чтение статуса и доступ к документам.

Балансы (iGaming/финансы)

Write: агрегат `UserBalance` с атомарными инкрементами/декрементами; идемпотентные ключи на операцию.
Read: кеш для «быстрого баланса»; для списания — прямое чтение из write (строгая согласованность).
Сага: депозиты/выводы координируются событиями, при сбоях — компенсации.


Чек-лист внедрения

  • Выделены агрегаты и инварианты write-модели.
  • Определены ключевые queries и спроектированы проекции под них.
  • Настроены outbox/CDC и идемпотентные процессоры проекций.
  • Есть план пересборки проекций (snapshot/replay).
  • SLO: латентность команд, лаг проекций, доступность read/write по отдельности.
  • Разделены права доступа и шифрование данных реализовано.
  • Алерты на DLQ/лаг/провалы дедупликации.
  • Тесты: контракты, проекции, хаос, реплей.

FAQ

Обязательно ли Event Sourcing для CQRS?
Нет. Можно строить на обычной БД + outbox/CDC.

Как бороться с рассинхронизацией?
Явно проектировать UX, измерять лаг проекций, давать критическим операциям читать из write.

Можно ли в одном сервисе держать и write, и read?
Да, физическое разделение — опционально; логическое разделение ответственности обязательно.

Что насчет транзакций между агрегатами?
Через саги и события; избегайте распределенных транзакций, если можно.


Итог

CQRS развязывает руки: тонкий, надежный путь записи с четкими инвариантами и быстрые, целевые чтения из материализованных проекций. Это повышает производительность, упрощает эволюцию и делает систему устойчивее к нагрузкам — если дисциплинированно управлять согласованностью, наблюдаемостью и миграциями.

Contact

Свяжитесь с нами

Обращайтесь по любым вопросам или за поддержкой.Мы всегда готовы помочь!

Начать интеграцию

Email — обязателен. Telegram или WhatsApp — по желанию.

Ваше имя необязательно
Email необязательно
Тема необязательно
Сообщение необязательно
Telegram необязательно
@
Если укажете Telegram — мы ответим и там, в дополнение к Email.
WhatsApp необязательно
Формат: +код страны и номер (например, +380XXXXXXXXX).

Нажимая кнопку, вы соглашаетесь на обработку данных.