Поделиться через


Шаблон CQRS

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

Контекст и проблема

В традиционной архитектуре для операций чтения и записи часто используется одна модель данных. Этот подход прост и подходит для базовых операций создания, чтения, обновления и удаления (CRUD).

Схема, показывающая традиционную архитектуру CRUD.

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

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

  • Конкуренция за блокировку: Параллельные операции с тем же набором данных могут вызвать конкуренцию за блокировку.

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

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

Объединение этих обязанностей может привести к чрезмерно сложной модели.

Решение

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

  • Общие сведения о командах. Команды должны представлять определенные бизнес-задачи, а не обновления данных низкого уровня. Например, в приложении для бронирования отеля используйте команду "Забронировать номер" вместо "Установить статус бронирования на Забронирован". Этот подход лучше передает намерение пользователя и согласует команды с бизнес-процессами. Чтобы обеспечить успешность выполнения команд, может потребоваться уточнить поток взаимодействия пользователя и логику на стороне сервера и рассмотреть асинхронную обработку.

    Область уточнения Рекомендация
    Проверка на стороне клиента Проверьте определенные условия перед отправкой команды, чтобы предотвратить очевидные сбои. Например, если номера недоступны, отключите кнопку "Забронировать" и предоставьте четкое, понятное сообщение в пользовательском интерфейсе, которое объясняет, почему бронирование невозможно. Эта настройка уменьшает ненужные запросы сервера и предоставляет немедленную обратную связь пользователям, что улучшает их взаимодействие.
    Логика на стороне сервера Расширьте бизнес-логику, чтобы обрабатывать пограничные случаи и сбои. Например, чтобы устранить условия гонки, такие как несколько пользователей, пытающихся забронировать последнюю доступную комнату, попробуйте добавить пользователей в список ожидания или предложить альтернативные варианты.
    Асинхронная обработка Обрабатывайте команды асинхронно, помещая их в очередь вместо синхронной обработки.
  • Общие сведения о запросах. Запросы никогда не изменяют данные. Вместо этого они возвращают объекты передачи данных (DTOs), которые представляют необходимые данные в удобном формате без какой-либо логики домена. Это разделение обязанностей упрощает проектирование и реализацию системы.

Отдельные модели чтения и модели записи

Разделение модели чтения от модели записи упрощает проектирование системы и реализацию путем решения конкретных проблем, связанных с записью данных и чтением данных. Это разделение повышает четкость, масштабируемость и производительность, но обеспечивает компромиссы. Например, средства формирования шаблонов, такие как платформы сопоставления с реляционными объектами (O/RM), не могут автоматически создавать код CQRS из схемы базы данных, поэтому вам нужна настраиваемая логика для преодоления разрыва.

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

Отдельные модели в одном хранилище данных

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

Схема с базовой архитектурой CQRS.

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

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

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

Отдельные модели в разных хранилищах данных

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

Схема, на которой показана архитектура CQRS с отдельными хранилищами данных для чтения и для записи.

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

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

Преимущества CQRS

  • Независимое масштабирование. CQRS позволяет моделям чтения и моделям записи масштабироваться независимо. Этот подход может помочь свести к минимуму конфликт блокировки и повысить производительность системы под нагрузкой.

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

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

  • Разделение проблем. Разделение обязанностей чтения и записи приводит к более чистым, более обслуживаемым моделям. Сторона записи обычно обрабатывает сложную бизнес-логику. Сторона чтения может оставаться простой и сосредоточенной на эффективности запросов.

  • Более простые запросы. При хранении материализованного представления в базе данных для чтения приложение может избежать сложных соединений при запросе.

Проблемы и рекомендации

Учтите следующие моменты, принимая решение о реализации этого шаблона.

  • Повышенная сложность. Основная концепция CQRS является простой, но она может привести к значительной сложности в разработке приложения, в частности при сочетании с шаблоном источника событий.

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

  • Конечная согласованность. Если базы данных чтения и базы данных записи разделены, данные чтения могут не отображать последние изменения немедленно. Эта задержка приводит к устаревшим данным. Убедитесь, что хранилище моделей чтения остается up-to-date с изменениями в хранилище моделей записи может быть сложной задачей. Кроме того, обнаружение и обработка сценариев, когда пользователь действует на устаревших данных, требует тщательного рассмотрения.

Когда следует использовать этот шаблон

Используйте этот шаблон, когда:

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

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

    • Модель записи содержит полный стек обработки команд с бизнес-логикой, проверкой входных данных и бизнес-проверкой. Модель записи может рассматривать набор связанных объектов как одну единицу для изменений данных, которая называется агрегатом в терминологии проектирования на основе домена. Модель записи также может помочь гарантировать, что эти объекты всегда находятся в согласованном состоянии.

    • Модель чтения не имеет бизнес-логики или стека проверки. Он возвращает DTO для использования в модели представления. Для моделей чтения и записи реализована итоговая согласованность.

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

  • У вас есть разделение проблем развития. CQRS позволяет командам работать независимо. Одна команда реализует сложную бизнес-логику в модели записи, а другая команда разрабатывает компоненты модели чтения и пользовательского интерфейса.

  • У вас есть развивающиеся системы. CQRS поддерживает системы, которые развиваются со временем. Он содержит новые версии модели, частые изменения бизнес-правил или другие изменения, не затрагивая существующие функциональные возможности.

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

Этот шаблон может быть не подходит, если:

  • Домен или бизнес-правила просты.

  • Достаточно простых операций пользовательского интерфейса и доступа к данным в стиле CRUD.

Проектирование рабочей нагрузки

Оцените, как использовать шаблон CQRS в проектировании рабочей нагрузки для решения целей и принципов, описанных в основных принципах Azure Well-Architected Framework. В следующей таблице приведены рекомендации по использованию этого шаблона для целей компонента "Эффективность производительности".

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

- Pe:05 Масштабирование и секционирование
- PE:08 производительность данных

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

Объедините паттерны Event Sourcing и CQRS.

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

  • Хранилище событий — это модель записи и единственный источник истины.

  • Модель чтения создает материализованные представления из этих событий, как правило, в высоко денормализованной форме. Эти представления оптимизируют получение данных путем настройки структур для запроса и отображения требований.

Преимущества объединения паттернов Event Sourcing и CQRS

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

Вместо прямого хранения текущего состояния система использует поток событий в качестве хранилища записи. Этот подход снижает конфликты обновлений для агрегатов и повышает производительность и масштабируемость. Система может асинхронно обрабатывать эти события для создания или обновления материализованных представлений для хранилища для чтения данных.

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

Рекомендации по объединению шаблонов событийного хранения и CQRS

Прежде чем объединить шаблон CQRS с шаблоном источника событий, ознакомьтесь со следующими рекомендациями.

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

  • Повышенная сложность: Объединение шаблонов CQRS с шаблоном обеспечения качества событий требует другого подхода к проектированию, что может сделать успешную реализацию более сложной. Необходимо написать код для создания, обработки и обработки событий, а также сборки или обновления представлений для модели чтения. Однако шаблон "Event Sourcing" упрощает моделирование домена и позволяет легко воссоздавать или создавать новые представления, сохраняя историю и намерение всех изменений данных.

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

Пример

В следующем коде показано извлечение из примера реализации 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 может содержать или самостоятельно создавать материализованные представления данных из модели записи.