Command and Query Responsibility Segregation (CQRS) pattern
Разделите операции, которые считывают данные из операций, которые обновляют данные, используя отдельные интерфейсы. Это может обеспечить максимальную производительность, масштабируемость и безопасность. Поддерживает эволюцию системы с течением времени благодаря большей гибкости и предотвращает появление команд обновления для конфликтов слияния на уровне домена.
Контекст и проблема
В традиционных системах управления данными обе команды (обновления данных) и запросы (запросы на данные) выполняются в отношении одного и того же набора объектов в одном хранилище данных. Эти объекты могут быть подмножеством строк в одной или нескольких таблицах в реляционной базе данных, такой как SQL Server.
Обычно в этих системах все операции создания, чтения, обновления и удаления (CRUD) применяются к тому же представлению объекта. Например, объект передачи данных (DTO), представляющий клиента, извлекается из хранилища данных на уровне доступа к данным (DAL) и отображается на экране. Пользователь обновляет некоторые поля DTO (возможно, путем привязки данных), а DTO затем сохраняется в хранилище данных с помощью DAL. Тот же DTO используется как для операций чтения, так и для записи. На рисунке показана традиционная архитектура CRUD.
Традиционные проекты CRUD хорошо работают, когда к операциям данных применяется только ограниченная бизнес-логика. Механизмы лесов, предоставляемые инструментами разработки, могут очень быстро создавать код доступа к данным, которые затем могут быть настроены по мере необходимости.
Однако традиционный подход CRUD имеет некоторые недостатки:
-
- Это часто означает, что существует несоответствие между представлениями чтения и записи данных, такими как дополнительные столбцы или свойства, которые необходимо обновить правильно, даже если они не требуются как часть операции.
- Он подвергает риску конфликты данных, когда записи блокируются в хранилище данных в совместном домене, где несколько участников работают параллельно в одном наборе данных. Или конфликты обновления, вызванные параллельными обновлениями, когда используется оптимистическая блокировка. Эти риски возрастают по мере увеличения сложности и пропускной способности системы. Кроме того, традиционный подход может отрицательно сказаться на производительности из-за нагрузки на хранилище данных и уровня доступа к данным и сложности запросов, необходимых для получения информации.
- Это может сделать управление безопасностью и разрешения более сложными, поскольку каждый объект подчиняется как операциям чтения, так и записи, которые могут вывести данные в неправильном контексте.
Решение
Синтаксис команды и запросов (CQRS) — это шаблон, который разделяет операции, которые считывают данные (запросы) из операций, которые обновляют данные (команды) с помощью отдельных интерфейсов. Это означает, что модели данных, используемые для запросов и обновлений, различны. Затем модели могут быть изолированы, как показано на следующем рисунке, хотя это не является абсолютным требованием.
По сравнению с единой моделью данных, используемой в системах на основе CRUD, использование отдельных моделей запросов и обновлений для данных в системах на основе CQRS упрощает проектирование и реализацию. Однако одним из недостатков является то, что в отличие от конструкций CRUD код CQRS не может автоматически генерироваться с использованием механизмов лесов.
Модель запроса для чтения данных и модель обновления для записи данных могут обращаться к одному и тому же физическому хранилищу, возможно, с использованием представлений SQL или путем создания прогнозов «на лету». Однако, как правило, разделить данные на разные физические магазины, чтобы максимизировать производительность, масштабируемость и безопасность, как показано на следующем рисунке.
Хранилище чтения может быть репликой только для чтения хранилища записи, или магазины чтения и записи могут иметь совсем другую структуру. Использование нескольких реплик только для чтения в хранилище чтения может значительно повысить производительность запросов и отзывчивость пользовательского интерфейса приложения, особенно в распределенных сценариях, где реплики только для чтения расположены близко к экземплярам приложения. Некоторые системы баз данных (SQL Server) предоставляют дополнительные функции, такие как резервные копии для повышения доступности.
Разделение хранилищ чтения и записи также позволяет каждому масштабировать соответствующим образом для соответствия нагрузке. Например, магазины чтения обычно сталкиваются с гораздо более высокой нагрузкой, чем записи.
Когда модель запроса / чтения содержит денормализованные данные (см. Materialized View pattern ), производительность максимизируется при чтении данных для каждого из представлений в приложении или при запросе данных в системе.
Проблемы и соображения
При принятии решения о реализации этого шаблона рассмотрите следующие моменты:
- Разделение хранилища данных на отдельные физические хранилища для операций чтения и записи может повысить производительность и безопасность системы, но это может добавить сложность с точки зрения отказоустойчивости и возможной согласованности. Хранилище прочитанной модели необходимо обновить, чтобы отразить изменения в хранилище модели записи, и может быть трудно обнаружить, когда пользователь выдал запрос на основе устаревших данных чтения, что означает, что операция не может быть завершена.
Описание возможной согласованности см. В Руководстве по совместимости данных (Data Consistency Primer) . - Рассмотрите возможность применения CQRS для ограниченных разделов вашей системы, где это будет наиболее ценно.
- Типичным подходом к развертыванию конечной согласованности является использование источника событий в сочетании с CQRS, чтобы модель записи была потоком событий, поддерживаемым только сообщением, управляемым выполнением команд. Эти события используются для обновления материализованных представлений, которые действуют как модель чтения. Дополнительные сведения см. В разделе « Составление событий и CQRS» .
Когда использовать этот шаблон
Используйте этот шаблон в следующих ситуациях:
- Совместные домены, где несколько операций выполняются параллельно по тем же данным. CQRS позволяет вам определять команды с достаточной степенью детализации для минимизации конфликтов слияния на уровне домена (любые конфликты, которые могут возникнуть, могут быть объединены командой), даже при обновлении того, что похоже на тот же тип данных.
- Пользовательские интерфейсы на основе задач, в которых пользователи руководствуются сложным процессом в виде серии шагов или со сложными моделями доменов. Кроме того, полезно для команд, уже знакомых с методами проектирования с использованием домена (DDD). Модель записи имеет полный стек обработки команд с бизнес-логикой, проверкой ввода и проверкой бизнеса, чтобы гарантировать, что все всегда согласовано для каждого из агрегатов (каждый кластер связанных объектов, рассматриваемых как единица для изменения данных) в модели записи , Модель чтения не имеет бизнес-логики или стека проверки и просто возвращает DTO для использования в модели представления. Модель считывания в конечном итоге согласуется с моделью записи.
- Сценарии, в которых производительность чтения данных должна быть точно настроена отдельно от производительности записи данных, особенно когда соотношение чтения / записи очень велико и когда требуется масштабирование по горизонтали. Например, во многих системах число операций чтения во много раз больше числа операций записи. Чтобы учесть это, рассмотрите возможность масштабирования модели чтения, но запустите модель записи только на одном или нескольких экземплярах. Небольшое количество экземпляров модели записи также помогает свести к минимуму возникновение конфликтов слияния.
- Сценарии, в которых одна команда разработчиков может сосредоточиться на сложной модели домена, которая является частью модели записи, а другая команда может сосредоточиться на модели чтения и пользовательских интерфейсах.
- Сценарии, в которых система, как ожидается, будет развиваться с течением времени и может содержать несколько версий модели или когда бизнес-правила меняются регулярно.
- Интеграция с другими системами, особенно в сочетании с источником событий, где временный сбой одной подсистемы не должен влиять на доступность других.
Этот шаблон не рекомендуется в следующих ситуациях:
- Если домен или бизнес-правила просты.
- Там, где достаточно простого пользовательского интерфейса в стиле CRUD и связанных с ним операций доступа к данным.
- Для реализации по всей системе. Существуют конкретные компоненты общего сценария управления данными, где CQRS может быть полезен, но он может добавить значительную и ненужную сложность, когда это не требуется.
Event Sourcing и CQRS
Шаблон CQRS часто используется вместе с шаблоном Sourcing. Системы на основе CQRS используют отдельные модели данных для чтения и записи, каждый из которых адаптирован к соответствующим задачам и часто расположен в физически раздельных магазинах. При использовании с шаблоном Sourcing Event хранилище событий является моделью записи и является официальным источником информации. Читаемая модель системы на основе CQRS обеспечивает материализованные представления данных, как правило, как сильно денормализованные представления. Эти представления адаптированы к интерфейсам и требованиям к отображению приложения, что помогает максимизировать производительность дисплея и запросов.
Использование потока событий в качестве хранилища записи, а не фактических данных в определенный момент времени, позволяет избежать конфликтов обновления в одном агрегате и максимизировать производительность и масштабируемость. События могут использоваться для асинхронного создания материализованных представлений данных, которые используются для заполнения хранилища чтения.
Поскольку хранилище событий является официальным источником информации, можно удалить материализованные представления и воспроизвести все прошлые события, чтобы создать новое представление текущего состояния, когда система эволюционирует, или когда модель чтения должна измениться. Материализованные представления представляют собой надежный кеш-память только для чтения данных.
При использовании CQRS в сочетании с шаблоном Sourcing Event рассмотрите следующее:
- Как и в любой системе, где хранилища записи и чтения являются отдельными, системы, основанные на этом шаблоне, только в конечном итоге согласуются. Между создаваемым событием и обновляемым хранилищем данных будет определенная задержка.
- Шаблон добавляет сложности, потому что код должен быть создан, чтобы инициировать и обрабатывать события, а также собирать или обновлять соответствующие представления или объекты, требуемые запросами или моделью чтения. Сложность шаблона CQRS при использовании с шаблоном Event Sourcing может усложнить успешную реализацию и требует другого подхода к проектированию систем. Тем не менее, поиск событий может упростить моделирование домена и упрощает пересоединение представлений или создание новых, поскольку намерение изменений в данных сохраняется.
- Генерирование материализованных представлений для использования в считываемой модели или прогнозах данных путем повторного воспроизведения и обработки событий для конкретных объектов или коллекций объектов может потребовать значительного времени обработки и использования ресурсов. Это особенно актуально, если требуется суммирование или анализ значений в течение длительного времени, поскольку все связанные события могут потребоваться изучить. Решите это, выполнив моментальные снимки данных через запланированные интервалы, такие как общее количество числа определенного действия, которое произошло, или текущее состояние объекта.
Пример
Следующий код показывает некоторые выдержки из примера реализации CQRS, в котором используются разные определения для моделей чтения и записи. Интерфейсы модели не диктуют какие-либо функции базовых хранилищ данных, и они могут развиваться и настраиваться независимо, потому что эти интерфейсы разделены.
Следующий код показывает определение модели чтения.
C #
// Query interface namespace ReadModel { public interface ProductsDao { ProductDisplay FindById(int productId); ICollection<ProductDisplay> FindByName(string name); ICollection<ProductInventory> FindOutOfStockProducts(); ICollection<ProductDisplay> FindRelatedProducts(int productId); } public class ProductDisplay { public int Id { get; set; } public string Name { get; set; } public string Description { get; set; } public decimal UnitPrice { get; set; } public bool IsOutOfStock { get; set; } public double UserRating { get; set; } } public class ProductInventory { public int Id { get; set; } public string Name { get; set; } public int CurrentStock { get; set; } } }
Система позволяет пользователям оценивать продукты. Код приложения делает это, используя RateProductкоманду, показанную в следующем коде.
C #
public interface ICommand { Guid Id { get; } } public class RateProduct : ICommand { public RateProduct() { this.Id = Guid.NewGuid(); } public Guid Id { get; set; } public int ProductId { get; set; } public int Rating { get; set; } public int UserId {get; set; } }
Система использует ProductsCommandHandlerкласс для обработки команд, отправленных приложением. Клиенты обычно отправляют команды в домен через систему обмена сообщениями, такую как очередь. Обработчик команд принимает эти команды и вызывает методы интерфейса домена. Гранулярность каждой команды предназначена для уменьшения вероятности противоречивых запросов. Следующий код показывает схему ProductsCommandHandlerкласса.
C #
public class ProductsCommandHandler : ICommandHandler<AddNewProduct>, ICommandHandler<RateProduct>, ICommandHandler<AddToInventory>, ICommandHandler<ConfirmItemShipped>, ICommandHandler<UpdateStockFromInventoryRecount> { private readonly IRepository<Product> repository; public ProductsCommandHandler (IRepository<Product> repository) { this.repository = repository; } void Handle (AddNewProduct command) { ... } void Handle (RateProduct command) { var product = repository.Find(command.ProductId); if (product != null) { product.RateProduct(command.UserId, command.Rating); repository.Save(product); } } void Handle (AddToInventory command) { ... } void Handle (ConfirmItemsShipped command) { ... } void Handle (UpdateStockFromInventoryRecount command) { ... } }
Следующий код показывает IProductsDomainинтерфейс из модели записи.
C #
public interface IProductsDomain { void AddNewProduct(int id, string name, string description, decimal price); void RateProduct(int userId, int rating); void AddToInventory(int productId, int quantity); void ConfirmItemsShipped(int productId, int quantity); void UpdateStockFromInventoryRecount(int productId, int updatedQuantity); }
Также обратите внимание, как IProductsDomainинтерфейс содержит методы, имеющие значение в домене. Как правило, в среде CRUD эти методы имеют общие имена, такие как Saveили Update, и имеют DTO как единственный аргумент. Подход CQRS может быть разработан для удовлетворения потребностей систем управления бизнесом и инвентаризацией этой организации.
Связанные шаблоны и руководство
Следующие шаблоны и рекомендации полезны при реализации этого шаблона:
- Для сравнения CQRS с другими архитектурными стилями, см стили архитектуры и CQRS стиль архитектуры .
- Руководствo по совместимости данных (Data Consistency Primer) . Объясняет проблемы, которые обычно возникают из-за возможной согласованности между хранилищами данных чтения и записи при использовании шаблона CQRS и как эти проблемы могут быть разрешены.
- Руководство по разделению данных . Описывает, как хранилища данных чтения и записи, используемые в шаблоне CQRS, можно разделить на разделы, которым можно управлять и получать доступ отдельно для улучшения масштабируемости, уменьшения конкуренции и оптимизации производительности.
- Шаблон источника событий . Описывает более подробно, как Event Sourcing можно использовать с шаблоном CQRS для упрощения задач в сложных доменах, одновременно повышая производительность, масштабируемость и оперативность. Как и как обеспечить согласованность транзакционных данных, сохраняя при этом все контрольные журналы и историю, которые могут позволить компенсирующие действия.
- Materialized View Pattern . Модель чтения CQRS-реализации может содержать материализованные представления данных модели записи, или модель чтения может использоваться для создания материализованных представлений.
- Образцы и практики руководства CQRS Journey . В частности, введение шаблона сегрегации ответов на запрос команды исследует шаблон и когда это полезно, а Epilogue: Lessons Learned помогает понять некоторые проблемы, возникающие при использовании этого шаблона.
- Post CQRS Мартина Фаулера , в котором объясняются основы шаблона и ссылки на другие полезные ресурсы.
-
Записи Грега Янга , в которых рассматриваются многие аспекты шаблона CQRS.
Original(english): https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs
Translation: Basic, need improvment
Status: Draft
Action: Vote to improve