CQRS (Command Query Responsibility Segregation) é um padrão de design que segrega operações de leitura e gravação para um armazenamento de dados em modelos de dados separados. Isso 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 arquiteturas tradicionais, um único modelo de dados é frequentemente usado para operações de leitura e gravação. Esta abordagem é simples e funciona bem para operações CRUD básicas (ver figura 1).
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 necessidades diferentes de desempenho e escala. Uma arquitetura CRUD tradicional não dá conta dessa assimetria. Coloca 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 no desempenho devido à carga no armazenamento de dados e na camada de acesso a dados e à complexidade das consultas necessárias para recuperar informações.
Preocupações de segurança: A gestão da segurança torna-se difícil quando as entidades estão sujeitas a operações de leitura e escrita. Essa sobreposição pode expor dados em contextos não intencionais.
A combinação destas responsabilidades pode resultar num modelo demasiado complicado que tenta fazer demasiado.
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 pela atualização dos 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 hotel, use "Reservar quarto de hotel" em vez de "Definir status de reserva como reservado". Essa abordagem reflete melhor a intenção por trás das ações do usuário e alinha os comandos com os 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 requinte | Recomendação |
---|---|
Validação do lado do cliente | Valide certas condições antes de enviar o comando para evitar falhas óbvias. Por exemplo, se não houver quartos disponíveis, desative o botão "Reservar" e forneça uma mensagem clara e fácil de usar na interface do usuário explicando por que a reserva não é possível. Esta configuração reduz os pedidos desnecessários do servidor e fornece feedback imediato aos utilizadores, melhorando a 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 comandos de processo de forma assíncrona colocando-os em uma fila, em vez de manipulá-los de forma síncrona. |
Entenda as consultas. As consultas nunca alteram os dados. Em vez disso, eles retornam DTOs (Data Transfer Objects) que apresentam os dados necessários em um formato conveniente, sem qualquer lógica de domínio. Esta clara separação de preocupações simplifica a conceção e implementação do sistema.
Compreender a separação de modelos de leitura e escrita
Separar o modelo de leitura do modelo de gravação simplifica o design e a implementação do sistema, abordando preocupações distintas para gravações e leituras de dados. Essa separação melhora a clareza, a escalabilidade e o desempenho, mas introduz algumas compensações. Por exemplo, ferramentas de andaimes como estruturas O/RM não podem gerar automaticamente código CQRS a partir de um esquema de banco de dados, exigindo lógica personalizada para preencher a lacuna.
As seções a seguir exploram duas abordagens principais para implementar a separação de modelos 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, onde os modelos de leitura e gravação compartilham um único banco de dados subjacente, mas mantêm lógicas distintas para suas operações. Ao definir preocupações separadas, essa estratégia aumenta a simplicidade e, ao mesmo tempo, 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 a partir do modelo de leitura enquanto depende de um armazenamento de dados compartilhado (ver figura 2).
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 questõ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 dos dados, otimizando a integridade transacional e os processos de negócios.
Modelo de leitura: Projetado para servir 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 melhora o desempenho e a capacidade de resposta da consulta, evitando a lógica do domínio.
Separação física de modelos em armazenamentos de dados separados
Uma implementação CQRS mais avançada 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 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 documentos para o armazenamento de dados de leitura e um banco de dados relacional para o armazenamento de dados de gravação (veja a figura 3).
Figura 3. Uma arquitetura CQRS com armazenamentos de dados de leitura e gravação separados.
Sincronizando armazenamentos de dados separados: Ao usar armazenamentos 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, geralmente não é possível recrutar agentes de mensagens e bancos de dados em uma única transação distribuída. Assim, pode haver desafios em garantir a consistência ao atualizar o banco de dados e publicar eventos. Para obter mais informações, consulte processamento de mensagens idempotentes.
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 de exibição materializada dos dados para evitar junções complexas ou mapeamentos 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
Escalonamento 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 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 apenas as entidades ou operações de domínio apropriadas tenham permissão para executar ações de gravação nos dados.
Separação das preocupações. Dividir as responsabilidades de leitura e gravação resulta em modelos mais limpos e fáceis de manter. 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 relativas à aplicação
Alguns desafios da implementação desse padrão incluem:
Aumento da complexidade. Embora o conceito central do CQRS seja simples, ele pode introduzir uma complexidade significativa no design do aplicativo, particularmente quando combinado com o padrão Event Sourcing.
Desafios de mensagens. Embora o sistema de mensagens não seja um requisito para o CQRS, você costuma usá-lo para processar comandos e publicar eventos de atualização. Quando o sistema de mensagens está envolvido, o sistema deve levar em conta possíveis problemas, como falhas de mensagens, duplicatas e tentativas. Consulte as orientações sobre de Filas de Prioridade para estratégias para lidar com comandos com prioridades variáveis.
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-toatualizado com as alterações no repositório de modelos de gravação pode ser um desafio. Além disso, detetar e manipular 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 onde vários usuários acessam e modificam os mesmos dados simultaneamente, o CQRS ajuda a reduzir os 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 guiam os usuários por 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, conhecida como um agregado
na terminologia de design controlado por 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 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 escrita.
Ajuste de desempenho: Os sistemas em que o desempenho das leituras de dados deve ser ajustado separadamente do desempenho das gravações de dados, especialmente quando o número de leituras é maior do que o número de gravações, se beneficiam 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 das preocupações de desenvolvimento: CQRS permite que as equipas 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 o modelo de leitura e os componentes da interface do usuário.
Sistemas em evolução: CQRS suporta sistemas que evoluem ao longo do tempo. Ele acomoda novas versões de modelos, alterações frequentes nas regras de negócios ou outras modificações sem afetar a funcionalidade existente.
Integração de sistemas: Os sistemas que se integram com outros subsistemas, especialmente aqueles que usam o Event Sourcing, 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 CQRS nas seguintes situações:
O domínio ou as regras de negócio são simples.
Uma interface de usuário simples no estilo CRUD e operações de acesso a dados são suficientes.
Design da carga de trabalho
Um arquiteto deve avaliar como usar o padrão CQRS no design de sua carga de trabalho para abordar as metas e os princípios abordados nos pilares do Azure Well-Architected Framework. Por exemplo:
Pilar | Como esse padrão suporta os objetivos do pilar |
---|---|
A Eficiência de Desempenho ajuda sua carga de trabalho a atender às demandas de forma eficiente por meio de otimizações em escala, dados e código. | A separação de operações de leitura e gravação em altas cargas de trabalho de leitura para gravação permite otimizações direcionadas de desempenho e dimensionamento para a finalidade específica de cada operação. - PE:05 Dimensionamento e particionamento - PE:08 Desempenho dos dados |
Como em qualquer decisão de design, considere quaisquer compensações em relação aos objetivos dos outros pilares que possam ser introduzidos com esse padrão.
Combinando sourcing de eventos e 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 repete esses eventos em ordem. Nesta combinação:
A loja de eventos é o modelo de escrita e a única fonte de verdade.
O modelo de leitura gera visões materializadas desses eventos, normalmente de forma altamente desnormalizada. Essas exibições otimizam a recuperação de dados adaptando estruturas para requisitos de consulta e exibição.
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 então criar um instantâneo em tempo real do estado atual. Esses instantâneos otimizam as consultas fornecendo visualizações eficientes e pré-computadas dos dados.
Em vez de armazenar diretamente o estado atual, o sistema usa um fluxo de eventos como armazenamento de gravação. Essa abordagem reduz os 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 leitura.
Como o repositório de eventos atua como a única fonte de verdade, você pode facilmente regenerar visualizações materializadas ou adaptar-se a mudanças no modelo de leitura reproduzindo eventos históricos. Em essência, as visualizaçõ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 Event Sourcing, avalie as seguintes considerações:
Consistência eventual: Como os armazenamentos de gravação e leitura são separados, as atualizações para o repositório de leitura podem ficar atrás da geração de eventos, resultando em consistência eventual.
Maior complexidade: Combinar CQRS com Event Sourcing 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 Event Sourcing simplifica a modelagem de domínio e permite que você reconstrua 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 visualizações: Gerar visualizações materializadas para o modelo de leitura pode consumir tempo e recursos significativos. O mesmo se aplica à projeção de dados através da reprodução e processamento de eventos para entidades ou coleções específicas. Este efeito aumenta quando os cálculos envolvem a análise ou soma de valores durante longos períodos, uma vez que 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 snapshots reduzem a necessidade de processar o histórico completo de eventos repetidamente, melhorando o desempenho.
Exemplo de padrão CQRS
O código seguinte mostra alguns extratos de um exemplo de uma implementação do CQRS que utiliza definições diferentes para os modelos de leitura e escrita. As interfaces do modelo não ditam quaisquer funcionalidades dos arquivos de dados subjacentes e podem evoluir e ser otimizadas de forma independente, uma vez que são interfaces separadas.
O código seguinte 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 utilizadores classificar os produtos. O código de aplicação realiza esta operação com o comando RateProduct
mostrado no código seguinte.
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 processar os comandos enviados pela aplicação. Por norma, os clientes enviam comandos para o domínio através de um sistema de mensagens, tal como uma fila. O processador de comandos aceita estes comandos e invoca métodos da interface de domínio. A granularidade de cada comando é concebida para reduzir a possibilidade de pedidos em conflito. O código seguinte mostra uma descriçã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óximos passos
Os padrões e as orientações que se seguem são úteis ao implementar este padrão:
- Particionamento de dados horizontal, vertical e funcional. Descreve as práticas recomendadas para dividir dados em partições que podem ser gerenciadas e acessadas separadamente para melhorar a escalabilidade, reduzir a contenção e otimizar o desempenho.
Recursos relacionados
Padrão de Origem do Evento. Descreve como usar o Event Sourcing com o padrão CQRS. Ele mostra como simplificar tarefas em domínios complexos enquanto melhora o desempenho, a escalabilidade e a capacidade de resposta. Ele também explica como fornecer consistência para dados transacionais enquanto mantém trilhas de auditoria completas e histórico que podem permitir ações de compensação.
Padrão de Vista Materializada. O modelo de leitura de uma implementação do CQRS pode conter vistas materializadas dos dados do modelo de escrita. Em alternativa, utilize o modelo de leitura para gerar vistas materializadas.