Compartilhar via


Padrão CQRS

CQRS (Segregação de Responsabilidade de Comando e Consulta) é um padrão de design que segrega operações de leitura e gravação de um armazenamento de dados em modelos de dados separados. Essa abordagem permite que cada modelo seja otimizado de forma independente e pode melhorar o desempenho, a escalabilidade e a segurança de um aplicativo.

Contexto e problema

Em uma arquitetura tradicional, um único modelo de dados geralmente é usado para operações de leitura e gravação. Essa abordagem é simples e é adequada para operações básicas de criação, leitura, atualização e exclusão (CRUD).

Diagrama que mostra uma arquitetura CRUD tradicional.

À medida que os aplicativos crescem, pode se tornar cada vez mais difícil otimizar operações de leitura e gravação em um único modelo de dados. As operações de leitura e gravação geralmente têm diferentes requisitos de desempenho e dimensionamento. Uma arquitetura CRUD tradicional não leva essa assimetria em conta, o que pode resultar nos seguintes desafios:

  • Incompatibilidade de dados: as representações de leitura e gravação de dados geralmente diferem. Alguns campos necessários durante as atualizações podem ser desnecessários durante operações de leitura.

  • Contenção de bloqueio: as operações paralelas no mesmo conjunto de dados podem causar contenção de bloqueio.

  • Problemas de desempenho: A abordagem tradicional pode ter um efeito negativo sobre o desempenho devido à carga no armazenamento de dados e na camada de acesso a dados e à complexidade das consultas necessárias para recuperar informações.

  • Desafios de segurança: Pode ser difícil gerenciar a segurança quando as entidades estão sujeitas a operações de leitura e gravação. Essa sobreposição pode expor dados em contextos não intencionais.

Combinar essas responsabilidades pode resultar em um modelo excessivamente complicado.

Solução

Use o padrão CQRS para separar operações de gravação ou comandos de operações de leitura ou consultas. Os comandos atualizam os dados. As consultas recuperam dados. O padrão CQRS é útil em cenários que exigem uma separação clara entre comandos e leituras.

  • Entenda os comandos. Os comandos devem representar tarefas comerciais específicas em vez de atualizações de dados de baixo nível. Por exemplo, em um aplicativo de reserva de hotéis, use o comando "Reservar quarto de hotel" em vez de "Definir ReservationStatus como Reservado". Essa abordagem captura melhor a intenção do usuário e alinha comandos com processos de negócios. Para ajudar a garantir que os comandos sejam bem-sucedidos, talvez seja necessário refinar o fluxo de interação do usuário e a lógica do lado do servidor e considerar o processamento assíncrono.

    Área de refinamento Recomendação
    Validação do lado do cliente Valide condições específicas antes de enviar o comando para evitar falhas óbvias. Por exemplo, se não houver salas disponíveis, desabilite o botão "Reservar" e forneça uma mensagem clara e amigável na interface do usuário que explique por que a reserva não é possível. Essa configuração reduz solicitações de servidor desnecessárias e fornece comentários imediatos aos usuários, o que aprimora sua experiência.
    Lógica do lado do servidor Aprimore a lógica de negócios para lidar com casos extremos e falhas de maneira eficiente. Por exemplo, para lidar com condições de concorrência, como vários usuários tentando reservar o último quarto disponível, considere adicionar usuários a uma lista de espera ou sugerir alternativas.
    Processamento assíncrono Processe comandos de forma assíncrona colocando-os em uma fila, em vez de tratá-los de forma síncrona.
  • Entenda as consultas. Consultas nunca alteram dados. Em vez disso, eles retornam DTOs (objetos de transferência de dados) que apresentam os dados necessários em um formato conveniente, sem nenhuma lógica de domínio. Essa separação distinta de responsabilidades simplifica o design e a implementação do sistema.

Modelos de leitura e modelos de gravação separados

Separar o modelo de leitura do modelo de gravação simplifica o design e a implementação do sistema, resolvendo preocupações específicas para gravações de dados e leituras de dados. Essa separação melhora a clareza, a escalabilidade e o desempenho, mas introduz compensações. Por exemplo, as ferramentas de scaffolding, como estruturas de mapeamento relacional de objeto (O/RM), não podem gerar automaticamente código CQRS de um esquema de banco de dados, portanto, você precisa de lógica personalizada para preencher a lacuna.

As seções a seguir descrevem duas abordagens primárias para implementar o modelo de leitura e a separação do modelo de gravação no CQRS. Cada abordagem tem benefícios e desafios exclusivos, como sincronização e gerenciamento de consistência.

Separar modelos em um único armazenamento de dados

Essa abordagem representa o nível fundamental do CQRS, em que os modelos de leitura e gravação compartilham um único banco de dados subjacente, mas mantêm uma lógica distinta para suas operações. Uma arquitetura CQRS básica permite que você delinee o modelo de gravação do modelo de leitura enquanto depende de um armazenamento de dados compartilhado.

Diagrama que mostra uma arquitetura CQRS básica.

Essa abordagem melhora a clareza, o desempenho e a escalabilidade definindo modelos distintos para lidar com preocupações de leitura e gravação.

  • Um modelo de gravação foi projetado para lidar com comandos que atualizam ou persistem dados. Ele inclui validação e lógica de domínio e ajuda a garantir a consistência de dados otimizando a integridade transacional e os processos de negócios.

  • Um modelo de leitura foi projetado para atender consultas para recuperar dados. Ele se concentra na geração de DTOs ou projeções otimizadas para a camada de apresentação. Ele aprimora o desempenho e a capacidade de resposta da consulta evitando a lógica de domínio.

Modelos separados em armazenamentos de dados diferentes

Uma implementação mais avançada do CQRS usa armazenamentos de dados distintos para os modelos de leitura e gravação. A separação dos armazenamentos de dados de leitura e gravação permite dimensionar cada modelo para corresponder à carga. Ele também permite que você use uma tecnologia de armazenamento diferente para cada armazenamento de dados. Você pode usar um banco de dados de documento para o armazenamento de dados de leitura e um banco de dados relacional para o armazenamento de dados de gravação.

Diagrama que mostra uma arquitetura CQRS com armazenamentos de dados de leitura separados e armazenamentos de dados de gravação.

Ao usar armazenamentos de dados separados, você deve garantir que ambos permaneçam sincronizados. Um padrão comum é fazer com que o modelo de gravação publique eventos ao atualizar o banco de dados, que o modelo de leitura usa para atualizar seus dados. Para obter mais informações sobre como usar eventos, consulte o estilo de arquitetura controlado por eventos. Como você geralmente não pode inscrever agentes de mensagens e bancos de dados em uma única transação distribuída, os desafios na consistência podem ocorrer quando você atualiza o banco de dados e os eventos de publicação. Para obter mais informações, consulte processamento de mensagens idempotente.

O armazenamento de dados de leitura pode empregar um esquema de dados próprio, otimizado para consultas. Por exemplo, ele pode armazenar uma exibição materializada dos dados para evitar junções complexas ou mapeamentos de O/RM. O armazenamento de dados de leitura pode ser uma réplica somente leitura do repositório de gravação ou ter uma estrutura diferente. Implantar várias réplicas somente leitura pode melhorar o desempenho, reduzindo a latência e aumentando a disponibilidade, especialmente em cenários distribuídos.

Benefícios do CQRS

  • Dimensionamento independente. O CQRS permite que os modelos de leitura e os modelos de gravação sejam dimensionados de forma independente. Essa abordagem pode ajudar a minimizar a contenção de bloqueio e melhorar o desempenho do sistema sob carga.

  • Esquemas de dados otimizados. As operações de leitura podem usar um esquema otimizado para consultas. As operações de gravação usam um esquema otimizado para atualizações.

  • Segurança. Ao separar leituras e gravações, você pode garantir que somente as entidades ou operações de domínio apropriadas tenham permissão para executar ações de gravação nos dados.

  • Separação de preocupações. Separar as responsabilidades de leitura e gravação resulta em modelos mais limpos e mantenedíveis. O lado de gravação normalmente lida com a lógica de negócios complexa. O lado de leitura pode permanecer simples e focado na eficiência das consultas.

  • Consultas mais simples. Quando você armazena uma exibição materializada no banco de dados de leitura, o aplicativo pode evitar junções complexas quando consulta.

Problemas e considerações

Considere os seguintes pontos ao decidir como implementar esse padrão:

  • Maior complexidade. O conceito principal do CQRS é simples, mas pode introduzir uma complexidade significativa no design do aplicativo, especificamente quando combinado com o padrão de Fornecimento de Eventos.

  • Desafios de mensagens. O sistema de mensagens não é um requisito para o CQRS, mas geralmente você o usa para processar comandos e publicar eventos de atualização. Quando as mensagens são incluídas, o sistema deve considerar possíveis problemas, como falhas de mensagem, duplicatas e novas tentativas. Para obter mais informações sobre estratégias para lidar com comandos que têm prioridades variadas, consulte As filas de prioridade.

  • Consistência eventual. Quando os bancos de dados de leitura e os bancos de dados de gravação são separados, os dados de leitura podem não mostrar as alterações mais recentes imediatamente. Esse atraso resulta em dados obsoletos. Garantir que o repositório de modelos de leitura permaneça atualizado com as alterações no repositório de modelos de gravação pode ser desafiador. Além disso, detectar e manipular cenários em que um usuário age em dados obsoletos requer uma consideração cuidadosa.

Quando usar esse padrão

Use esse padrão quando:

  • Você trabalha em ambientes colaborativos. Em ambientes em que vários usuários acessam e modificam os mesmos dados simultaneamente, o CQRS ajuda a reduzir conflitos de mesclagem. Os comandos podem incluir granularidade suficiente para evitar conflitos e o sistema pode resolver conflitos que ocorrem dentro da lógica de comando.

  • Você tem interfaces de usuário baseadas em tarefas. Aplicativos que orientam os usuários por meio de processos complexos como uma série de etapas ou com modelos de domínio complexos se beneficiam do CQRS.

    • O modelo de gravação tem uma pilha completa de processamento de comandos com a lógica de negócios, validação de entrada e validação de negócios. O modelo de gravação pode tratar um conjunto de objetos associados como uma única unidade para alterações de dados, que é conhecida como uma agregação na terminologia de design controlada pelo domínio. O modelo de gravação também pode ajudar a garantir que esses objetos estejam sempre em um estado consistente.

    • O modelo de leitura não tem nenhuma lógica de negócios ou pilha de validação. Ele retorna um DTO para uso em um modelo de exibição. O modelo de leitura é, eventualmente, consistente com o modelo de gravação.

  • Você precisa de ajuste de desempenho. Sistemas em que o desempenho das leituras de dados deve ser ajustado separadamente do desempenho de gravações de dados se beneficiam do CQRS. Esse padrão é especialmente benéfico quando o número de leituras é maior que o número de gravações. O modelo de leitura é dimensionado horizontalmente para lidar com grandes volumes de consulta. O modelo de escrita opera em menos instâncias para minimizar conflitos de fusão e manter a consistência.

  • Você tem problemas de separação de desenvolvimento. O CQRS permite que as equipes trabalhem de forma independente. Uma equipe implementa a lógica de negócios complexa no modelo de gravação e outra equipe desenvolve os componentes de modelo de leitura e interface do usuário.

  • Você tem sistemas em evolução. O CQRS dá suporte a sistemas que evoluem ao longo do tempo. Ele acomoda novas versões de modelo, alterações frequentes nas regras de negócios ou outras modificações sem afetar a funcionalidade existente.

  • Você precisa de integração do sistema: Os sistemas que se integram a outros subsistemas, especialmente sistemas que usam o padrão de Fornecimento de Eventos, permanecem disponíveis mesmo se um subsistema falhar temporariamente. O CQRS isola falhas, o que impede que um único componente afete todo o sistema.

O padrão pode não ser adequado nestes casos:

  • O domínio ou as regras de negócios são simples.

  • Uma interface de usuário em estilo CRUD simples e as operações de acesso aos dados são suficientes.

Design de carga de trabalho

Avalie como usar o padrão CQRS no design de uma carga de trabalho para atender às metas e princípios abordados nos pilares do Azure Well-Architected Framework. A tabela a seguir fornece diretrizes sobre como esse padrão dá suporte às metas do pilar de Eficiência de Desempenho.

Pilar Como esse padrão apoia os objetivos do pilar
A Eficiência de Desempenho ajuda sua carga de trabalho a atender com eficiência às demandas por meio de otimizações no dimensionamento, nos dados e no código. A separação de operações de leitura e operações de gravação em cargas de trabalho de leitura para gravação altas permite otimizações de desempenho e dimensionamento direcionadas para a finalidade específica de cada operação.

- PE:05 Dimensionamento e particionamento
- PE:08 Performance de dados

Considere as compensações em relação às metas dos outros pilares que esse padrão pode introduzir.

Combinar os padrões de Fornecimento de Eventos e CQRS

Algumas implementações do CQRS incorporam o padrão de Fornecimento de Eventos. Esse padrão armazena o estado do sistema como uma série cronológica de eventos. Cada evento captura as alterações feitas nos dados em um momento específico. Para determinar o estado atual, o sistema reproduza esses eventos em ordem. Nesta configuração:

  • O repositório de eventos é o modelo de gravação e a única fonte de verdade.

  • O modelo de leitura gera exibições materializadas desses eventos, normalmente em uma forma altamente desnormalizada. Essas exibições otimizam a recuperação de dados adaptando as estruturas para atender às necessidades de consulta e exibição.

Benefícios de combinar os padrões de Fornecimento de Eventos e CQRS

Os mesmos eventos que atualizam o modelo de gravação podem servir como entradas para o modelo de leitura. O modelo de leitura pode criar um instantâneo em tempo real do estado atual. Esses instantâneos otimizam as consultas fornecendo exibições eficientes e pré-computadas dos dados.

Em vez de armazenar diretamente o estado atual, o sistema usa um fluxo de eventos como o repositório de gravação. Essa abordagem reduz conflitos de atualização em agregações e melhora o desempenho e a escalabilidade. O sistema pode processar esses eventos de forma assíncrona para criar ou atualizar exibições materializadas para o armazenamento de dados de leitura.

Como o repositório de eventos atua como a única fonte de verdade, você pode regenerar facilmente exibições materializadas ou adaptar-se às alterações no modelo de leitura reproduzindo eventos históricos. Basicamente, as visões materializadas funcionam como um cache durável, somente para leitura, otimizado para consultas rápidas e eficientes.

Considerações sobre como combinar os padrões de Fornecimento de Eventos e CQRS

Antes de combinar o padrão CQRS com o padrão de fornecimento de eventos, avalie as seguintes considerações:

  • Consistência eventual: Como os armazenamentos de dados de gravação e leitura são separados, as atualizações no armazenamento de dados de leitura podem ficar para trás da geração de eventos. Esse atraso resulta em uma consistência eventual.

  • Maior complexidade: Combinar o padrão CQRS com o padrão de Fornecimento de Eventos requer uma abordagem de design diferente, o que pode tornar uma implementação bem-sucedida mais desafiadora. Você deve escrever código para gerar, processar e manipular eventos e montar ou atualizar exibições para o modelo de leitura. No entanto, o padrão de Fornecimento de Eventos simplifica a modelagem de domínio e permite que você recompile ou crie novas exibições facilmente preservando o histórico e a intenção de todas as alterações de dados.

  • Desempenho da geração de exibição: Gerar exibições materializadas para o modelo de leitura pode consumir tempo e recursos significativos. O mesmo se aplica à projeção de dados reproduzindo e processando eventos para entidades ou coleções específicas. A complexidade aumenta quando os cálculos envolvem analisar ou resumir valores em longos períodos, pois todos os eventos relacionados devem ser examinados. Implemente capturas instantâneas dos dados em intervalos regulares. Por exemplo, armazene o estado atual de uma entidade ou instantâneos periódicos de totais agregados, que é o número de vezes que ocorre uma ação específica. Os instantâneos reduzem a necessidade de processar o histórico de eventos completo repetidamente, o que melhora o desempenho.

Exemplo

O código a seguir mostra os extratos de um exemplo de uma implementação CQRS que usa definições diferentes para os modelos de leitura e os modelos de gravação. As interfaces de modelo não ditam os recursos dos armazenamentos de dados subjacentes e podem evoluir e ser ajustadas independentemente porque essas interfaces são separadas.

O código a seguir mostra a definição do modelo de leitura.

// 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; }
  }
}

O sistema permite aos usuários avaliar produtos. O código do aplicativo faz isso usando o RateProduct comando mostrado no código a seguir.

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; }
}

O sistema usa a ProductsCommandHandler classe para lidar com os comandos que o aplicativo envia. Normalmente, os clientes enviam comandos para o domínio através de um sistema de mensagens, como uma fila. O manipulador de comando aceita esses comandos e invoca métodos da interface de domínio. A granularidade de cada comando é projetada para reduzir a chance de solicitações conflitantes. O código a seguir mostra um esboço da classe 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)
  {
    ...
  }
}

Próxima etapa

As seguintes informações podem ser relevantes quando você implementa esse padrão:

  • As diretrizes de particionamento de dados descrevem as práticas recomendadas de como dividir dados em partições que você pode gerenciar e acessar separadamente para melhorar a escalabilidade, reduzir a contenção e otimizar o desempenho.
  • Padrão de Fornecimento do Evento. Esse padrão descreve como simplificar tarefas em domínios complexos e melhorar o desempenho, a escalabilidade e a capacidade de resposta. Ele também explica como fornecer consistência para dados transacionais, mantendo trilhas de auditoria completas e histórico que podem habilitar ações de compensação.

  • Padrão de Exibição Materializada. Esse padrão cria exibições pré-preenchidas, conhecidas como exibições materializadas, para consulta eficiente e extração de dados de um ou mais armazenamentos de dados. O modelo de leitura de uma implementação CQRS pode conter exibições materializadas dos dados do modelo de gravação, ou o modelo de leitura pode ser utilizado para gerar exibições materializadas.