АрхитектураПроектирование системАрхитектор распределённых систем

В сервисе заказов аналитические запросы конкурируют с оформлением заказов за ресурсы базы. Как разделение м...

В сервисе заказов аналитические запросы конкурируют с оформлением заказов за ресурсы базы. Как разделение моделей чтения и записи снижает это влияние?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

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

Это уменьшает конкуренцию за CPU, блокировки, соединения и дисковый ввод-вывод основной базы. Цена подхода — усложнение синхронизации, возможная задержка обновления проекции и необходимость обрабатывать повторную доставку событий.

Исторический контекст

Классическая CRUD-модель использует одну схему и один набор объектов для чтения и изменения данных. Она проста, пока нагрузка и требования к запросам остаются однородными.

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

Постановка проблемы

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

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

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

Подробное решение

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

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

Важна гарантия доставки изменения. Если событие записывается только во внешнюю очередь после коммита, сбой между этими операциями может привести к потере обновления. Для устранения этого разрыва применяют паттерн transactional outbox: событие сохраняется в той же транзакции, что и бизнес-изменение, а отдельный процесс доставляет его повторно при необходимости.

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

Чтения из проекции часто имеют eventual consistency. Сразу после записи пользователь может временно не увидеть заказ в аналитическом представлении. Если конкретному сценарию нужна немедленная видимость, его направляют в модель записи либо используют версию состояния и ждут, пока проекция достигнет этой версии.

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

Главный компромисс — производительность и независимое масштабирование чтения против сложности согласования данных. CQRS оправдан, когда профили нагрузки, структура запросов или требования к масштабированию действительно различаются; для простого CRUD-сервиса он часто создаёт больше проблем, чем решает.

Ситуация из практики

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

Увеличение ресурсов было быстрым, но не устраняло конкуренцию и плохо защищало от дальнейшего роста. Дополнительные индексы ускоряли отдельные запросы, однако замедляли записи и увеличивали объём хранимых данных.

Выбрали отдельную аналитическую проекцию, обновляемую из надёжно доставляемых событий. Оформление заказа осталось в транзакционной базе, а отчёты стали выполняться независимо; при этом в интерфейсе явно указали, что аналитические показатели обновляются с небольшой задержкой. Решение потребовало контроля задержки проекции, повторной обработки событий и процедуры её полного восстановления.

Что кандидаты часто упускают

  1. Обязательно ли CQRS означает eventual consistency?

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

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

  1. Что произойдёт, если обработчик события остановится на середине обновления проекции?

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

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

  1. Почему нельзя считать отдельную read-модель просто кэшем?

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

Кроме того, для read-модели нужно контролировать версию, задержку и полноту обработки изменений. Поэтому её отказ может означать не только временное замедление, но и необходимость восстановления или повторной материализации.