Разделение ответственности запросов команд (CQRS) — это шаблон проектирования, который разделяет операции чтения и записи для хранилища данных в отдельные модели данных. Это позволяет оптимизировать каждую модель независимо и повысить производительность, масштабируемость и безопасность приложения.
Контекст и проблема
В традиционных архитектурах для операций чтения и записи часто используется одна модель данных. Этот подход прост и хорошо подходит для основных операций CRUD (см. рисунок 1).
Рис. 1. Традиционная архитектура CRUD.
Однако по мере роста приложений оптимизация операций чтения и записи в одной модели данных становится все более сложной. Операции чтения и записи часто имеют разные потребности в производительности и масштабировании. Традиционная архитектура CRUD не учитывает эту асимметрию. Это приводит к нескольким проблемам:
несоответствие данных: часто различаются представления данных чтения и записи. Некоторые поля, необходимые во время обновления, могут быть ненужными во время чтения.
конфликт блокировки: параллельные операции с тем же набором данных могут вызвать конфликт блокировки.
проблемы с производительностью: традиционный подход может негативно повлиять на производительность из-за нагрузки на хранилище данных и уровень доступа к данным, а также сложность запросов, необходимых для получения информации.
проблемы безопасности: управление безопасностью становится сложной, когда сущности подвергаются операциям чтения и записи. Это перекрытие может предоставлять данные в непреднамеренных контекстах.
Объединение этих обязанностей может привести к чрезмерно сложной модели, которая пытается сделать слишком много.
Решение
Используйте шаблон CQRS для разделения операций записи (команд) от операций чтения (запросов). Команды отвечают за обновление данных.
Общие сведения о командах. Команды должны представлять определенные бизнес-задачи, а не обновления данных низкого уровня. Например, в приложении для бронирования отеля используйте "Book hotel room" вместо "Set BookingStatus to ReservedStatus to Reserved". Этот подход лучше отражает намерение действий пользователей и сопоставляет команды с бизнес-процессами. Чтобы обеспечить успешное выполнение команд, может потребоваться уточнить поток взаимодействия пользователя, логику на стороне сервера и рассмотреть асинхронную обработку.
Область уточнения | Рекомендация |
---|---|
Проверка на стороне клиента | Проверьте определенные условия перед отправкой команды, чтобы предотвратить очевидные сбои. Например, если номера недоступны, отключите кнопку "Книга" и предоставьте четкое понятное сообщение в пользовательском интерфейсе, объясняющее, почему резервирование невозможно. Эта настройка уменьшает ненужные запросы сервера и предоставляет немедленную обратную связь пользователям, повышая их взаимодействие. |
Логика на стороне сервера | Расширьте бизнес-логику, чтобы обрабатывать пограничные случаи и сбои. Например, чтобы устранить условия гонки (несколько пользователей пытаются забронировать последнюю доступную комнату), попробуйте добавить пользователей в список ожидания или предложить альтернативные варианты. |
Асинхронная обработка | Кроме того, команды обработки асинхронно, поместив их в очередь, а не синхронно. |
Общие сведения о запросах. Запросы никогда не изменяют данные. Вместо этого они возвращают объекты передачи данных (DTOs), которые представляют необходимые данные в удобном формате без какой-либо логики домена. Это четкое разделение проблем упрощает проектирование и реализацию системы.
Общие сведения о разделениях моделей чтения и записи
Разделение модели чтения от модели записи упрощает проектирование системы и реализацию, устраняя различные проблемы записи и чтения данных. Это разделение повышает четкость, масштабируемость и производительность, но представляет некоторые компромиссы. Например, средства формирования шаблонов, такие как платформы O/RM, не могут автоматически создавать код CQRS из схемы базы данных, требуя пользовательской логики для преодоления разрыва.
В следующих разделах рассматриваются два основных подхода к реализации разделения моделей чтения и записи в CQRS. Каждый подход поставляется с уникальными преимуществами и проблемами, такими как синхронизация и управление согласованности.
Разделение моделей в одном хранилище данных
Этот подход представляет базовый уровень CQRS, где модели чтения и записи совместно используют одну базовую базу данных, но поддерживают отдельную логику для своих операций. Определив отдельные проблемы, эта стратегия повышает простоту при предоставлении преимуществ в масштабируемости и производительности для типичных вариантов использования. Базовая архитектура CQRS позволяет очертить модель записи из модели чтения при использовании общего хранилища данных (см. рис. 2).
Рис. 2. Базовая архитектура CQRS с одним хранилищем данных.
Этот подход повышает четкость, производительность и масштабируемость путем определения различных моделей обработки проблем записи и чтения:
модель записи: предназначен для обработки команд, которые обновляют или сохраняют данные. Он включает проверку, логику домена и обеспечивает согласованность данных путем оптимизации для целостности транзакций и бизнес-процессов.
модель чтения: Предназначено для обслуживания запросов для получения данных. Он посвящен созданию объектов DTOs (объектов передачи данных) или проекций, оптимизированных для слоя презентации. Это повышает производительность запросов и скорость реагирования, избегая логики домена.
Физическое разделение моделей в отдельных хранилищах данных
Более расширенная реализация CQRS использует различные хранилища данных для моделей чтения и записи. Разделение хранилищ данных чтения и записи позволяет масштабировать каждый из них для сопоставления нагрузки. Она также позволяет использовать разные технологии хранения для каждого хранилища данных. Базу данных документов можно использовать для чтения хранилища данных и реляционной базы данных для хранилища данных записи (см. рисунок 3).
Рис. 3. Архитектура CQRS с отдельными хранилищами данных чтения и записи.
Синхронизация отдельных хранилищ данных: При использовании отдельных хранилищ необходимо убедиться, что оба хранилища остаются в синхронизации. Распространенный шаблон — создание событий публикации модели записи при каждом обновлении базы данных, которую модель чтения использует для обновления своих данных. Дополнительные сведения об использовании событий см. в стиле архитектуры на основе событий. Однако обычно вы не можете заручиться брокерами сообщений и базами данных в одну распределенную транзакцию. Таким образом, при обновлении базы данных и публикации могут возникнуть проблемы, связанные с обеспечением согласованности. Дополнительные сведения см. в идемпотентной обработке сообщений.
чтение хранилища данных: хранилище данных чтения может использовать собственную схему данных, оптимизированную для запросов. Например, он может хранить материализованное представление данных, чтобы избежать сложных соединений или сопоставлений O/RM. Хранилище чтения может быть репликой хранилища записи только для чтения или иметь другую структуру. Развертывание нескольких реплик только для чтения может повысить производительность, уменьшая задержку и повышая доступность, особенно в распределенных сценариях.
Преимущества CQRS
Независимое масштабирование. CQRS позволяет моделям чтения и записи масштабироваться независимо, что может помочь свести к минимуму конфликт блокировки и повысить производительность системы под нагрузкой.
Оптимизация схем данных. Операции чтения могут использовать схему, оптимизированную для запросов. Операции записи используют схему, оптимизированную для обновлений.
Безопасность. Разделив операции чтения и записи, вы можете убедиться, что только соответствующие сущности или операции домена имеют разрешение на выполнение действий записи данных.
Четкое разделение зон ответственности. Разделение обязанностей чтения и записи приводит к более чистым, более обслуживаемым моделям. Сторона записи обычно обрабатывает сложную бизнес-логику, а сторона чтения может оставаться простой и сосредоточиться на эффективности запросов.
Более простые запросы. При хранении материализованного представления в базе данных чтения приложение может избежать сложных соединений при запросе.
Проблемы и рекомендации по реализации
Ниже приведены некоторые проблемы реализации этого шаблона:
повышение сложности. Хотя основная концепция CQRS проста, она может привести к значительной сложности в проектировании приложения, особенно при сочетании с шаблоном источника событий.
проблемы обмена сообщениями. Хотя обмен сообщениями не является обязательным для CQRS, его часто используют для обработки команд и публикации событий обновления. При использовании обмена сообщениями система должна учитывать потенциальные проблемы, такие как сбои сообщений, дубликаты и повторные попытки. Ознакомьтесь с руководством по очередям приоритетов стратегии обработки команд с различными приоритетами.
Итоговая согласованность. Если базы данных чтения и записи разделены, данные чтения могут не отражать последние изменения немедленно, что приводит к устаревшим данным. Обеспечение того, что хранилище моделей чтения остается up-to-date с изменениями в хранилище моделей записи может быть сложной задачей. Кроме того, обнаружение и обработка сценариев, когда пользователь действует на устаревших данных, требует тщательного рассмотрения.
Когда следует использовать шаблон CQRS
Шаблон CQRS полезен в сценариях, требующих четкого разделения между изменениями данных (командами) и запросами данных (считывает). Рекомендуется использовать CQRS в следующих ситуациях:
домены совместной работы: в средах, где несколько пользователей обращаются к одному и тому же данным одновременно, CQRS помогает уменьшить конфликты слиянием. Команды могут включать достаточную степень детализации, чтобы предотвратить конфликты, и система может разрешить все, что возникает в логике команды.
пользовательские интерфейсы на основе задач: приложения, которые направляют пользователей через сложные процессы в виде ряда шагов или сложных моделей предметных областей, преимущества CQRS.
Модель записи содержит полный стек обработки команд с бизнес-логикой, проверкой входных данных и бизнес-проверкой. Модель записи может рассматривать набор связанных объектов как одну единицу для изменений данных, известной как агрегатная в терминологии проектирования на основе домена. Модель записи также может гарантировать, что эти объекты всегда находятся в согласованном состоянии.
Модель чтения не имеет бизнес-логики или стека проверки. Он возвращает DTO для использования в модели представления. Для моделей чтения и записи реализована итоговая согласованность.
настройка производительности: системы, где производительность операций чтения данных должна быть точно настроена отдельно от производительности операций записи данных, особенно если число операций чтения больше количества операций записи, преимущество от CQRS. Модель чтения горизонтально масштабируется для обработки больших томов запросов, а модель записи выполняется на меньшем количестве экземпляров, чтобы свести к минимуму конфликты слияния и обеспечить согласованность.
разделение проблем разработки: CQRS позволяет командам работать независимо. Одна команда фокусируется на реализации сложной бизнес-логики в модели записи, а другая разрабатывает модель чтения и компоненты пользовательского интерфейса.
Развивающиеся системы: CQRS поддерживает системы, которые развиваются с течением времени. Он содержит новые версии модели, частые изменения бизнес-правил или другие изменения, не затрагивая существующие функциональные возможности.
интеграция системы: системы, которые интегрируются с другими подсистемами, особенно при использовании источника событий, остаются доступными, даже если подсистема временно завершается сбоем. CQRS изолирует сбои, предотвращая влияние одного компонента на всю систему.
Если не использовать CQRS
Избегайте CQRS в следующих ситуациях:
Домен или бизнес-правила просты.
Достаточно простых операций пользовательского интерфейса и доступа к данным в стиле CRUD.
Проектирование рабочей нагрузки
Архитектор должен оценить, как использовать шаблон CQRS в проектировании рабочей нагрузки для решения целей и принципов, описанных в Well-Architected платформы Azure. Например:
Принцип | Как этот шаблон поддерживает цели основных компонентов |
---|---|
Эффективность производительности помогает рабочей нагрузке эффективно соответствовать требованиям путем оптимизации масштабирования, данных, кода. | Разделение операций чтения и записи в высокоуровневых рабочих нагрузках чтения и записи обеспечивает целевую производительность и оптимизацию масштабирования для конкретной цели каждой операции. - Pe:05 Масштабирование и секционирование - Производительность данных PE:08 |
Как и любое решение по проектированию, рассмотрите любые компромиссы по целям других столпов, которые могут быть представлены с этим шаблоном.
Объединение источников событий и CQRS
Некоторые реализации CQRS включают шаблон событий Sourcing, который сохраняет состояние системы в виде хронологического ряда событий. Каждое событие фиксирует изменения, внесенные в данные в определенное время. Чтобы определить текущее состояние, система воспроизводит эти события в порядке. В этом сочетании:
Хранилище событий — это модель записи и единственный источник истины.
модели чтения создает материализованные представления из этих событий, как правило, в высоко денормализованной форме. Эти представления оптимизируют получение данных путем настройки структур для запроса и отображения требований.
Преимущества объединения источников событий и CQRS
Те же события, которые обновляют модель записи, могут служить входными данными для модели чтения. Затем модель чтения может создать моментальный снимок текущего состояния в режиме реального времени. Эти моментальные снимки оптимизируют запросы, предоставляя эффективные предварительно вычисляемые представления данных.
Вместо прямого хранения текущего состояния система использует поток событий в качестве хранилища записи. Этот подход снижает конфликты обновлений для агрегатов и повышает производительность и масштабируемость. Система может обрабатывать эти события асинхронно для создания или обновления материализованных представлений для хранилища чтения.
Так как хранилище событий выступает в качестве единственного источника истины, вы можете легко создать материализованные представления или адаптироваться к изменениям в модели чтения путем повторения исторических событий. В сущности материализованные представления работают как устойчивый кэш только для чтения, оптимизированный для быстрых и эффективных запросов.
Рекомендации по сочетанию источников событий и CQRS
Прежде чем объединить шаблон CQRS с шаблоном источника событий, ознакомьтесь со следующими рекомендациями.
Итоговая согласованность: Так как хранилища записи и чтения разделены, обновления в хранилище чтения могут отстать от создания событий, что приведет к конечной согласованности.
повышенная сложность: объединение CQRS с набором событий требует другого подхода к проектированию, что может сделать успешную реализацию более сложной. Необходимо написать код для создания, обработки и обработки событий, а также сборки или обновления представлений для модели чтения. Однако при выборе событий упрощается моделирование домена и позволяет легко перестраивать или создавать новые представления, сохраняя журнал и намерение всех изменений данных.
производительность создания представлений: создание материализованных представлений для модели чтения может потреблять значительное время и ресурсы. Это же относится к проецированием данных путем повторения и обработки событий для определенных сущностей или коллекций. Этот эффект увеличивается, когда вычисления включают анализ или суммирование значений в течение длительных периодов, так как все связанные события должны быть проверены. Реализуйте моментальные снимки данных через регулярные интервалы. Например, храните периодические моментальные снимки агрегированных итогов (количество определенных действий) или текущее состояние сущности. Моментальные снимки снижают необходимость многократной обработки полного журнала событий, что повышает производительность.
Пример шаблона CQRS
Следующий фрагмент кода взят из реализации подхода CQRS, в рамках которой применены разные определения для моделей чтения и записи. Интерфейсы модели не налагают ограничения на функции хранилищ данных, и их можно изменять или конфигурировать независимо друг от друга, так как эти интерфейсы разделены.
В следующем коде представлено определение модели чтения.
// 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
, как показано ниже.
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
.
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)
{
...
}
}
Следующие шаги
При реализации этого шаблона будут полезны следующие шаблоны и рекомендации:
- Горизонтальное, вертикальное и функциональное секционирование данных. Описывает рекомендации по разделению данных на секции, к которым можно управлять и обращаться отдельно, чтобы повысить масштабируемость, уменьшить количество разных результатов и оптимизировать производительность.
Связанные ресурсы
Шаблон источников событий. Описывает использование источника событий с шаблоном CQRS. В нем показано, как упростить задачи в сложных доменах, повышая производительность, масштабируемость и скорость реагирования. В нем также объясняется, как обеспечить согласованность транзакционных данных при сохранении полных следов аудита и журнала, которые могут включать компенсирующие действия.
Materialized View Pattern (Шаблон материализованного представления). Модель чтения в реализации CQRS может содержать или самостоятельно создавать материализованные представления данных из модели записи.