События и очереди

4 мин чтения ·

CQRS: когда быстро читать и правильно записывать — разные задачи

Заказ уже принят, а в кабинете ещё старый статус. Почему разделение записи и чтения иногда ускоряет продукт — и когда оно создаёт больше проблем, чем решает.

Покупатель оформил заказ и сразу открыл историю покупок. Заказ уже принят, но на экране ещё прежняя информация. Для разработчика это может быть ожидаемая задержка обновления представления. Для клиента — подозрение, что деньги списали, а заказ потеряли.

Вокруг такого расхождения и стоит обсуждать CQRS. Не с вопроса «как нам развести базы», а с более простого: действительно ли чтение и изменение данных требуют разных решений, и какой ценой мы готовы обеспечить скорость каждого?

Слева операции с заказами, в центре данные, справа подготовленные представления и пользователи
Слева данные меняются по правилам бизнеса, справа готовятся представления для чтения. В реальной системе обе части могут использовать одну базу; иллюстрация показывает разделение ответственности, а не обязательную инфраструктуру.

Одна модель вынуждена выполнять две разные работы

Когда система принимает заказ, она должна проверить правила: действителен ли товар, можно ли применить скидку, достаточно ли прав у пользователя, допустим ли переход статуса. Ошибка здесь может стоить денег. Это путь записи: команда просит систему изменить состояние, а система либо принимает изменение, либо отказывает.

Когда клиент открывает список заказов, ему нужна другая форма данных: номер, сумма, текущий статус, дата доставки. Для отчёта руководителю вообще важны агрегированные показатели. Заставлять каждый экран восстанавливать всё из сложной модели записи бывает дорого.

CQRS — Command Query Responsibility Segregation — предлагает отделить операции изменения состояния от запросов на чтение. Но это не обязательство создавать две базы данных. Начать можно с разных объектов, запросов и обработчиков поверх одной базы. Физическое разделение появляется лишь тогда, когда для него есть причина.

Когда разделение окупается

Представим, что заказы записываются сравнительно редко, зато клиентские кабинеты, поддержка и внутренние панели постоянно их читают. Сложные запросы могут начать мешать транзакционным операциям. Тогда имеет смысл готовить специальные представления под конкретные сценарии чтения и масштабировать этот путь отдельно.

В другом продукте проблема может быть не в нагрузке, а в скорости изменений. Команде тяжело добавлять новые поля в отчёт, не затрагивая критичные бизнес-правила оформления заказа. Отдельная модель чтения даёт пространство для таких изменений.

Однако сам CQRS не ускоряет ничего по определению. Если запросов мало, а простая таблица справляется, новые обработчики и проекции будут расходом без заметной отдачи.

Цена отдельного представления — его актуальность

Если запись и чтение используют разные хранилища, изменения ещё нужно доставить в читаемую модель. Обычно для этого применяют события или поток изменений. Между подтверждением команды и обновлением экрана может появиться задержка.

У этой задержки есть имя — eventual consistency. Это не ошибка сама по себе, но клиенту требуется понятное поведение. После создания заказа можно сразу показать подтверждённый результат команды, явно обозначить статус обработки или временно читать критичную информацию из источника записи.

Самая плохая версия CQRS — когда команда решила, что экран «когда-нибудь обновится», а продукт не объяснил клиенту, что происходит. При оплате и управлении остатками требования к согласованности будут существенно строже, чем у обзорного отчёта.

Кто восстановит экран после сбоя

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

Если используется публикация событий из транзакционной базы, нельзя забывать о риске: данные сохранились, а событие не ушло. Для таких случаев существует Transactional Outbox. Но и с ним получателю потребуются идемпотентная обработка и мониторинг отставания.

Отдельно стоит измерять не только время ответа чтения, но и возраст данных в представлении. Быстрый экран со вчерашним остатком на складе не обязательно полезнее медленного.

Сначала исправьте запрос, потом меняйте архитектуру

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

Если ответ сводится к тому, что «CQRS — современный паттерн», лучше оставить простой CRUD. Разделение имеет смысл, когда оно снимает уже заметное ограничение и компания готова поддерживать дополнительный путь доставки и проверки данных.

CQRS полезен не тем, что у системы появляются красивые стрелки в две стороны. Он позволяет перестать ломать сложные правила записи ради удобства чтения — при условии, что цена рассинхронизации известна заранее.

Сравнить варианты от одной базы до раздельных хранилищ можно в описании CQRS в Microsoft Azure Architecture Center.