Editar

Compartilhar via


Padrão CQRS

Armazenamento do Azure

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

Contexto e problema

Em arquiteturas tradicionais, um único modelo de dados geralmente é usado para operações de leitura e gravação. Essa abordagem é simples e funciona bem para operações CRUD básicas (ver a figura 1).

Diagrama que mostra uma arquitetura CRUD tradicional.
Figura 1. Uma arquitetura CRUD tradicional.

No entanto, à medida que os aplicativos crescem, a otimização das operações de leitura e gravação em um único modelo de dados torna-se cada vez mais desafiadora. As operações de leitura e gravação geralmente têm diferentes necessidades de desempenho e dimensionamento. Uma arquitetura CRUD tradicional não conta para essa assímetria. Isso leva a vários 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 as leituras.

  • Contenção de bloqueio: 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.

  • Questões de segurança: Gerenciar a segurança torna-se difícil 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 que tenta fazer muito.

Solução

Use o padrão CQRS para separar operações de gravação (comandos) de operações de leitura (consultas). Os comandos são responsáveis por atualizar dados. As consultas são responsáveis pela recuperação de dados.

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 "Reservar quarto de hotel" em vez de "Definir ReservationStatus como Reservado". Essa abordagem reflete melhor a intenção por trás das ações do usuário e alinha comandos com processos de negócios. Para garantir que os comandos sejam bem-sucedidos, talvez seja necessário refinar o fluxo de interação do usuário, 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 determinadas condições antes de enviar o comando para evitar falhas óbvias. Por exemplo, se nenhuma sala estiver disponível, desabilite o botão "Reservar" e forneça uma mensagem clara e amigável na interface do usuário explicando 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, aprimorando sua experiência.
Lógica do lado do servidor Aprimore a lógica de negócios para lidar com casos de borda e falhas normalmente. Por exemplo, para lidar com as condições de corrida (vários usuários tentando reservar a última sala disponível), considere adicionar usuários a uma lista de espera ou sugerir opções alternativas.
Processamento assíncrono Você também pode processar 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 clara separação de preocupações simplifica o design e a implementação do sistema.

Entender a separação de modelo de leitura e gravação

Separar o modelo de leitura do modelo de gravação simplifica o design e a implementação do sistema, resolvendo preocupações distintas com gravações e leituras de dados. Essa separação melhora a clareza, a escalabilidade e o desempenho, mas apresenta algumas compensações. Por exemplo, ferramentas de scaffolding como estruturas O/RM não podem gerar automaticamente código CQRS de um esquema de banco de dados, exigindo lógica personalizada para fazer a ponte da lacuna.

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

Separação de 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. Ao definir preocupações separadas, essa estratégia aprimora a simplicidade ao mesmo tempo em que oferece benefícios em escalabilidade e desempenho para casos de uso típicos. Uma arquitetura CQRS básica permite delinear o modelo de gravação do modelo de leitura enquanto depende de um armazenamento de dados compartilhado (ver a figura 2).

Diagrama que mostra uma arquitetura CQRS básica.
Figura 2. Uma arquitetura CQRS básica com um único armazenamento de dados.

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

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

  • Modelo de leitura: Projetado para atender consultas para recuperar dados. Ele se concentra na geração de DTOs (objetos de transferência de dados) 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.

Separação física de modelos em armazenamentos de dados separados

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 que você dimensione cada um 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 (ver a figura 3).

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

Sincronizar armazenamentos de dados separados: Ao usar repositórios separados, você deve garantir que ambos permaneçam sincronizados. Um padrão comum é fazer com que o modelo de gravação publique eventos sempre que 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 estilo de arquitetura controlado por eventos. No entanto, você geralmente não pode inscrever agentes de mensagens e bancos de dados em uma única transação distribuída. Portanto, pode haver desafios para garantir a consistência ao atualizar o banco de dados e publicar eventos. Para obter mais informações, consulte de processamento de mensagens idempotente.

Armazenamento de dados de leitura: O armazenamento de dados de leitura pode usar seu próprio esquema de dados 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 repositório de leitura pode ser uma réplica somente leitura do repositório de gravação ou ter uma estrutura diferente. A implantação de 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 gravação sejam dimensionados de forma independente, o que pode ajudar a minimizar a contenção de bloqueios 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.

  • Divisão de problemas. Dividir as responsabilidades de leitura e gravação resulta em modelos mais limpos e mantêveis. O lado de gravação normalmente lida com lógica de negócios complexa, enquanto o lado de leitura pode permanecer simples e focado na eficiência da consulta.

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

Questões e considerações sobre implementação

Alguns desafios para implementar esse padrão incluem:

  • maior complexidade. Embora o conceito principal do CQRS seja simples, ele pode introduzir uma complexidade significativa no design do aplicativo, especialmente quando combinado com o padrão de Fornecimento de Eventos.

  • desafios de mensagens. Embora o sistema de mensagens não seja um requisito para o CQRS, você geralmente o usa para processar comandos e publicar eventos de atualização. Quando as mensagens estão envolvidas, o sistema deve considerar possíveis problemas, como falhas de mensagem, duplicatas e novas tentativas. Consulte as diretrizes sobre filas de prioridade para estratégias para lidar com comandos com prioridades variadas.

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

Quando usar o padrão CQRS

O padrão CQRS é útil em cenários que exigem uma separação clara entre modificações de dados (comandos) e consultas de dados (leituras). Considere o uso do CQRS nas seguintes situações:

  • domínios 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 qualquer um que surja dentro da lógica de comando.

  • 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 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, conhecido como uma agregação na terminologia de design orientada pelo domínio. O modelo de gravação também pode 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.

  • Ajuste de desempenho: Sistemas em que o desempenho das leituras de dados deve ser ajustado separadamente do desempenho de gravações de dados, especialmente quando o número de leituras é maior que o número de gravações, beneficiam-se do CQRS. O modelo de leitura é dimensionado horizontalmente para lidar com grandes volumes de consulta, enquanto o modelo de gravação é executado em menos instâncias para minimizar conflitos de mesclagem e manter a consistência.

  • separação de preocupações de desenvolvimento: CQRS permite que as equipes trabalhem de forma independente. Uma equipe se concentra na implementação da lógica de negócios complexa no modelo de gravação, enquanto outra desenvolve os componentes do modelo de leitura e da interface do usuário.

  • sistemas em evoluçã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.

  • Integração do sistema: Sistemas que se integram a outros subsistemas, especialmente aqueles que usam o Fornecimento de Eventos, permanecem disponíveis mesmo se um subsistema falhar temporariamente. O CQRS isola falhas, impedindo que um único componente afete todo o sistema.

Quando não usar CQRS

Evite o CQRS nas seguintes situações:

  • 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

Um arquiteto deve avaliar como usar o padrão CQRS no design de sua carga de trabalho para atender às metas e princípios abordados nos pilares do Azure Well-Architected Framework. Por exemplo:

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 em dimensionamento, dados e código. A separação das operações de leitura e gravação em altas cargas de trabalho de leitura para gravação permite desempenho direcionado e otimizações de escala para a finalidade específica de cada operação.

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

Tal como acontece com qualquer decisão de design, considere quaisquer compensações em relação aos objetivos dos outros pilares que possam ser introduzidos com este padrão.

Combinando o fornecimento de eventos e o CQRS

Algumas implementações do CQRS incorporam o padrão Event Sourcing, que armazena o estado do sistema como uma série cronológica de eventos. Cada evento captura as alterações feitas nos dados em um determinado momento. Para determinar o estado atual, o sistema reproduza esses eventos em ordem. Nesta combinaçã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 estruturas para consultar e exibir requisitos.

Benefícios da combinação 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 repositório 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. Em essência, as exibições materializadas funcionam como um cache durável e somente leitura otimizado para consultas rápidas e eficientes.

Considerações ao combinar o fornecimento de eventos e o 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 repositórios de gravação e leitura são separados, as atualizações no repositório de leitura podem ficar para trás da geração de eventos, resultando em consistência eventual.

  • Maior complexidade: a combinação de CQRS com o Fornecimento de Eventos requer uma abordagem de design diferente, o que pode tornar a 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 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. Esse efeito aumenta quando os cálculos envolvem analisar ou resumir valores em longos períodos, pois todos os eventos relacionados devem ser examinados. Implemente instantâneos dos dados em intervalos regulares. Por exemplo, armazene instantâneos periódicos de totais agregados (o número de vezes que uma ação específica ocorre) ou o estado atual de uma entidade. Os instantâneos reduzem a necessidade de processar o histórico de eventos completo repetidamente, melhorando o desempenho.

Exemplo de padrão CQRS

O código a seguir mostra alguns extratos de um exemplo de uma implementação CQRS que utiliza diferentes definições para os modelos de leitura e gravação. As interfaces modelo não ditarão quaisquer recursos dos repositórios de dados subjacentes, e elas podem evoluir e terem ajustes finos de maneira independente porque essas interfaces estã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 utilizando o comando RateProduct 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 utiliza a classe ProductsCommandHandler para lidar com comandos enviados pelo aplicativo. 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 uma estrutura de tópicos 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óximas etapas

Os seguintes padrões e diretrizes serão úteis ao implementar esse padrão:

  • Padrão de Fornecimento do Evento. Descreve como usar o Fornecimento de Eventos com o padrão CQRS. Ele mostra como simplificar tarefas em domínios complexos, melhorando 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. 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.