Compartir vía


Patrón CQRS

La segregación de responsabilidades de consulta de comandos (CQRS) es un patrón de diseño que separa las operaciones de lectura y escritura de un almacén de datos en modelos de datos independientes. Este enfoque permite optimizar cada modelo de forma independiente y puede mejorar el rendimiento, la escalabilidad y la seguridad de una aplicación.

Contexto y problema

En una arquitectura tradicional, a menudo se usa un único modelo de datos para las operaciones de lectura y escritura. Este enfoque es sencillo y es adecuado para las operaciones básicas de creación, lectura, actualización y eliminación (CRUD).

Diagrama que muestra una arquitectura CRUD tradicional.

A medida que crecen las aplicaciones, puede resultar cada vez más difícil optimizar las operaciones de lectura y escritura en un único modelo de datos. Las operaciones de lectura y escritura suelen tener distintos requisitos de rendimiento y escalado. Una arquitectura CRUD tradicional no tiene en cuenta esta asimetría, lo que puede dar lugar a los siguientes desafíos:

  • Error de coincidencia de datos: las representaciones de lectura y escritura de los datos a menudo difieren. Es posible que algunos campos necesarios durante las actualizaciones sean innecesarios durante las operaciones de lectura.

  • Contención de bloqueo: las operaciones paralelas en el mismo conjunto de datos pueden provocar contención de bloqueo.

  • Problemas de rendimiento: El enfoque tradicional puede tener un efecto negativo en el rendimiento debido a la carga en el almacén de datos y la capa de acceso a datos, y la complejidad de las consultas necesarias para recuperar información.

  • Desafíos de seguridad: Puede ser difícil administrar la seguridad cuando las entidades están sujetas a operaciones de lectura y escritura. Esta superposición puede exponer datos en contextos no deseados.

La combinación de estas responsabilidades puede dar lugar a un modelo demasiado complicado.

Solución

Use el patrón CQRS para separar las operaciones de escritura o los comandos de las operaciones de lectura o las consultas. Los comandos actualizan los datos. Las consultas recuperan datos. El patrón CQRS es útil en escenarios que requieren una separación clara entre comandos y lecturas.

  • Comprender los comandos. Los comandos deben representar tareas empresariales específicas en lugar de actualizaciones de datos de bajo nivel. Por ejemplo, en una aplicación de reserva de hoteles, use el comando "Reservar habitación de hotel" en lugar de "Establecer ReservationStatus en Reservado". Este enfoque captura mejor la intención del usuario y alinea los comandos con los procesos empresariales. Para ayudar a garantizar que los comandos son correctos, es posible que tenga que refinar el flujo de interacción del usuario y la lógica del lado servidor y considerar el procesamiento asincrónico.

    Área de mejora Recomendación
    Validación del lado cliente Valide condiciones específicas antes de enviar el comando para evitar errores obvios. Por ejemplo, si no hay habitaciones disponibles, deshabilite el botón "Reservar" y proporcione un mensaje claro y fácil de usar en la interfaz de usuario que explica por qué no es posible reservar. Esta configuración reduce las solicitudes de servidor innecesarias y proporciona comentarios inmediatos a los usuarios, lo que mejora su experiencia.
    Lógica del lado servidor Mejore la lógica de negocios para controlar los casos perimetrales y los errores correctamente. Por ejemplo, para abordar las condiciones de competencia, como varios usuarios que intentan reservar la última sala disponible, considere la posibilidad de agregar a los usuarios a una lista de espera o sugerir alternativas.
    Procesamiento asincrónico Procese los comandos de forma asincrónica colocándolos en una cola, en lugar de controlarlos de forma sincrónica.
  • Descripción de las consultas. Las consultas nunca modifican los datos. En su lugar, devuelven objetos de transferencia de datos (DTO) que presentan los datos necesarios en un formato conveniente, sin ninguna lógica de dominio. Esta separación distinta de responsabilidades simplifica el diseño y la implementación del sistema.

Modelos de lectura independientes y modelos de escritura

La separación del modelo de lectura del modelo de escritura simplifica el diseño y la implementación del sistema mediante la solución de problemas específicos para las escrituras de datos y las lecturas de datos. Esta separación mejora la claridad, la escalabilidad y el rendimiento, pero introduce compromisos. Por ejemplo, las herramientas de scaffolding como marcos de asignación relacional de objetos (O/RM) no pueden generar automáticamente código CQRS a partir de un esquema de base de datos, por lo que necesita lógica personalizada para salvar la brecha.

En las secciones siguientes se describen dos enfoques principales para implementar la separación de modelos de lectura y escritura en CQRS. Cada enfoque tiene ventajas y desafíos únicos, como la administración de sincronización y coherencia.

Separar modelos en un único almacén de datos

Este enfoque representa el nivel fundamental de CQRS, donde los modelos de lectura y escritura comparten una base de datos subyacente única, pero mantienen una lógica distinta para sus operaciones. Una arquitectura básica de CQRS permite delimitar el modelo de escritura del modelo de lectura mientras se basa en un almacén de datos compartido.

Diagrama que muestra una arquitectura básica de CQRS.

Este enfoque mejora la claridad, el rendimiento y la escalabilidad definiendo modelos distintos para controlar los problemas de lectura y escritura.

  • Un modelo de escritura está diseñado para controlar los comandos que actualizan o conservan los datos. Incluye la validación y la lógica de dominio, y ayuda a garantizar la coherencia de los datos mediante la optimización de la integridad transaccional y los procesos empresariales.

  • Un modelo de lectura está diseñado para servir consultas para recuperar datos. Se centra en generar DTO o proyecciones optimizadas para la capa de presentación. Mejora el rendimiento y la capacidad de respuesta de las consultas evitando la lógica de dominio.

Separar modelos en diferentes almacenes de datos

Una implementación de CQRS más avanzada usa almacenes de datos distintos para los modelos de lectura y escritura. La separación de los almacenes de datos de lectura y escritura permite escalar cada modelo para que coincida con la carga. También permite usar una tecnología de almacenamiento diferente para cada almacén de datos. Puede usar una base de datos de documentos para el almacén de datos de lectura y una base de datos relacional para el almacén de datos de escritura.

Diagrama que muestra una arquitectura de CQRS con almacenes de datos de lectura independientes y almacenes de datos de escritura.

Al usar almacenes de datos independientes, debe asegurarse de que ambos permanezcan sincronizados. Un patrón común es hacer que el modelo de escritura publique eventos cuando actualiza la base de datos, que el modelo de lectura usa para actualizar sus datos. Para obtener más información sobre cómo usar eventos, consulte Estilo de arquitectura controlada por eventos. Dado que normalmente no puede inscribir agentes de mensajes y bases de datos en una sola transacción distribuida, pueden producirse desafíos en la coherencia al actualizar la base de datos y publicar eventos. Para obtener más información, vea procesamiento de mensajes idempotentes.

El almacén de datos de lectura puede usar su propio esquema de datos optimizado para consultas. Por ejemplo, puede almacenar una vista materializada de los datos para evitar uniones complejas o asignaciones de O/RM. El almacén de datos de lectura puede ser una réplica de solo lectura del almacén de escritura o tener una estructura diferente. La implementación de varias réplicas de solo lectura puede mejorar el rendimiento al reducir la latencia y aumentar la disponibilidad, especialmente en escenarios distribuidos.

Ventajas de CQRS

  • Escalado independiente. CQRS permite que los modelos de lectura y escritura se escalen de forma independiente. Este enfoque puede ayudar a minimizar la contención de bloqueos y mejorar el rendimiento del sistema bajo carga.

  • Esquemas de datos optimizados. Las operaciones de lectura pueden usar un esquema optimizado para consultas. Las operaciones de escritura usan un esquema optimizado para actualizaciones.

  • Seguridad. Al separar las lecturas y escrituras, puede asegurarse de que solo las entidades o operaciones de dominio adecuadas tengan permiso para realizar acciones de escritura en los datos.

  • Separación de intereses. Separar las responsabilidades de lectura y escritura da como resultado modelos más limpios y fáciles de mantener. El lado de escritura normalmente controla la lógica de negocios compleja. El lado de lectura puede seguir siendo sencillo y centrado en la eficacia de las consultas.

  • Consultas más sencillas. Al almacenar una vista materializada en la base de datos de lectura, la aplicación puede evitar combinaciones complejas cuando realiza consultas.

Problemas y consideraciones

Tenga en cuenta los siguientes puntos a medida que decida cómo implementar este patrón:

  • Mayor complejidad. El concepto básico de CQRS es sencillo, pero puede introducir una complejidad significativa en el diseño de la aplicación, específicamente cuando se combina con el patrón Event Sourcing.

  • Desafíos de mensajería. La mensajería no es un requisito para CQRS, pero a menudo se usa para procesar comandos y publicar eventos de actualización. Cuando se incluye la mensajería, el sistema debe tener en cuenta posibles problemas, como errores de mensaje, duplicados y reintentos. Para más información sobre las estrategias para controlar los comandos que tienen prioridades variables, vea Colas de prioridad.

  • Coherencia final. Cuando se separan las bases de datos de lectura y escritura, es posible que los datos de lectura no muestren los cambios más recientes inmediatamente. Este retraso da como resultado datos obsoletos. Asegurarse de que el almacén de modelos de lectura permanece actualizado con los cambios en el almacén de modelos de escritura puede ser difícil. Además, detectar y controlar escenarios en los que un usuario actúa sobre datos obsoletos requiere una consideración cuidadosa.

Cuándo usar este patrón

Use este patrón en los siguientes supuestos:

  • Trabaja en entornos de colaboración. En entornos en los que varios usuarios acceden y modifican los mismos datos simultáneamente, CQRS ayuda a reducir los conflictos de combinación. Los comandos pueden incluir una granularidad suficiente para evitar conflictos y el sistema puede resolver los conflictos que se producen dentro de la lógica de comandos.

  • Tienes interfaces de usuario basadas en tareas. Las aplicaciones que guían a los usuarios a través de procesos complejos como una serie de pasos o con modelos de dominio complejos se benefician de CQRS.

    • El modelo de escritura tiene una pila de procesamiento de comandos completa con lógica de negocios, validación de entrada y validación empresarial. El modelo de escritura podría tratar un conjunto de objetos asociados como una sola unidad para los cambios de datos, lo que se conoce como un agregado en la terminología de diseño controlado por dominio. El modelo de escritura también puede ayudar a garantizar que estos objetos estén siempre en un estado coherente.

    • El modelo de lectura no tiene ninguna lógica de negocios ni pila de validación. Devuelve un DTO para su uso en un modelo de vista. El modelo de lectura tiene coherencia final con el modelo de escritura.

  • Debe realizar la optimización del rendimiento. Los sistemas en los que el rendimiento de las lecturas de datos se deben ajustar independientemente del rendimiento de las escrituras de datos se benefician de CQRS. Este patrón es especialmente beneficioso cuando el número de lecturas es mayor que el número de escrituras. El modelo de lectura se escala horizontalmente para controlar grandes volúmenes de consultas. El modelo de escritura funciona en menos instancias para minimizar los conflictos de fusión y mantener la consistencia.

  • Tiene separación de preocupaciones de desarrollo. CQRS permite a los equipos trabajar de forma independiente. Un equipo implementa la lógica de negocios compleja en el modelo de escritura y otro equipo desarrolla los componentes de la interfaz de usuario y el modelo de lectura.

  • Tiene sistemas en evolución. CQRS admite sistemas que evolucionan con el tiempo. Admite nuevas versiones del modelo, cambios frecuentes en las reglas de negocio u otras modificaciones sin afectar a la funcionalidad existente.

  • Necesita la integración del sistema: Los sistemas que se integran con otros subsistemas, especialmente los sistemas que usan el patrón Event Sourcing, siguen estando disponibles incluso si se produce un error temporal en un subsistema. CQRS aísla los errores, lo que impide que un único componente afecte a todo el sistema.

Este patrón podría no ser adecuado cuando:

  • El dominio o las reglas de negocio son simples.

  • Es suficiente con una interfaz de usuario simple, de estilo CRUD, y las operaciones relacionadas de acceso a datos.

Diseño de cargas de trabajo

Evalúe cómo usar el patrón CQRS en el diseño de una carga de trabajo para abordar los objetivos y principios descritos en los pilares de Azure Well-Architected Framework. En la tabla siguiente se proporciona orientación sobre cómo este patrón apoya los objetivos del pilar Eficiencia de Rendimiento.

Fundamento Cómo apoya este patrón los objetivos de los pilares
La eficiencia del rendimiento ayuda a la carga de trabajo a satisfacer de forma eficaz las demandas a través de optimizaciones en el escalado, los datos y el código. La separación de las operaciones de lectura y las operaciones de escritura en cargas de trabajo de lectura a escritura elevadas permite optimizar el rendimiento y el escalado específicos de cada operación.

- PE:05 Escalado y particionamiento
- PE:08 Rendimiento de datos

Considere las ventajas y desventajas de los objetivos de los otros pilares que este patrón podría introducir.

Combinar los patrones de Event Sourcing y CQRS

Algunas implementaciones de CQRS incorporan el patrón Event Sourcing. Este patrón almacena el estado del sistema como una serie cronológica de eventos. Cada evento captura los cambios realizados en los datos en un momento específico. Para determinar el estado actual, el sistema reproduce estos eventos en orden. En esta configuración:

  • El almacén de eventos es el modelo de escritura y la fuente única de la verdad.

  • El modelo de lectura genera vistas materializadas a partir de estos eventos, normalmente en forma altamente desnormalizada. Estas vistas optimizan la recuperación de datos mediante la adaptación de estructuras para consultar y mostrar los requisitos.

Ventajas de combinar los patrones de Event Sourcing y CQRS

Los mismos eventos que actualizan el modelo de escritura pueden servir como entradas para el modelo de lectura. A continuación, el modelo de lectura puede crear una instantánea en tiempo real del estado actual. Estas instantáneas optimizan las consultas al proporcionar vistas eficaces y precomputadas de los datos.

En lugar de almacenar directamente el estado actual, el sistema usa una secuencia de eventos como almacén de escritura. Este enfoque reduce los conflictos de actualización en los agregados y mejora el rendimiento y la escalabilidad. El sistema puede procesar estos eventos de forma asincrónica para compilar o actualizar vistas materializadas para el almacén de datos de lectura.

Dado que el almacén de eventos actúa como el único origen de la verdad, puede volver a generar fácilmente vistas materializadas o adaptarse a los cambios en el modelo de lectura mediante la reproducción de eventos históricos. Básicamente, las vistas materializadas funcionan como una caché duradera y de solo lectura optimizada para consultas rápidas y eficaces.

Consideraciones sobre cómo combinar los patrones de Event Sourcing y CQRS

Antes de combinar el patrón CQRS con el patrón Event Sourcing, evalúe las consideraciones siguientes:

  • Coherencia final: Dado que los almacenes de datos de escritura y lectura son independientes, es posible que las actualizaciones del almacén de datos de lectura se retrasan tras la generación de eventos. Este retraso da como resultado una coherencia final.

  • Mayor complejidad: La combinación del patrón CQRS con el patrón Event Sourcing requiere un enfoque de diseño diferente, lo que puede hacer que una implementación correcta sea más difícil. Debe escribir código para generar, procesar y controlar eventos y ensamblar o actualizar vistas para el modelo de lectura. Sin embargo, el patrón Event Sourcing simplifica el modelado de dominio y le permite recompilar o crear nuevas vistas fácilmente conservando el historial y la intención de todos los cambios de datos.

  • Rendimiento de la generación de vistas: Generar vistas materializadas para el modelo de lectura puede consumir un tiempo y recursos significativos. Lo mismo se aplica a la proyección de datos mediante la reproducción y el procesamiento de eventos para entidades o colecciones específicas. La complejidad aumenta cuando los cálculos implican analizar o sumar valores durante largos períodos, ya que se deben examinar todos los eventos relacionados. Realice registros instantáneos de los datos a intervalos regulares. Por ejemplo, almacene el estado actual de una entidad o instantáneas periódicas de totales agregados, que es el número de veces que se produce una acción específica. Las instantáneas reducen la necesidad de procesar el historial de eventos completo repetidamente, lo que mejora el rendimiento.

Ejemplo

En el código siguiente se muestran los extractos de un ejemplo de una implementación de CQRS que usa definiciones diferentes para los modelos de lectura y los modelos de escritura. Las interfaces de modelo no dictan características de los almacenes de datos subyacentes y pueden evolucionar y ajustarse de forma independiente porque estas interfaces son independientes.

El código siguiente muestra la definición del modelo de lectura.

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

El sistema permite a los usuarios valorar los productos. El código de la aplicación lo hace mediante el RateProduct comando que se muestra en el código siguiente.

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

El sistema usa la ProductsCommandHandler clase para controlar los comandos que envía la aplicación. Normalmente, los clientes envían comandos al dominio a través de un sistema de mensajería como, por ejemplo, una cola. El controlador de comandos acepta estos comandos e invoca los métodos de la interfaz de dominio. La granularidad de cada comando está diseñada para reducir la posibilidad de que haya solicitudes en conflicto. El código siguiente muestra un esquema de la clase 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)
  {
    ...
  }
}

Paso siguiente

La siguiente información puede ser relevante al implementar este patrón:

  • En la guía de creación de particiones de datos se describen los procedimientos recomendados para dividir los datos en particiones que puede administrar y acceder por separado para mejorar la escalabilidad, reducir la contención y optimizar el rendimiento.
  • Patrón de originación de eventos. Este patrón describe cómo simplificar las tareas en dominios complejos y mejorar el rendimiento, la escalabilidad y la capacidad de respuesta. También se explica cómo proporcionar coherencia para los datos transaccionales al tiempo que se mantienen los registros de auditoría completos y el historial que pueden habilitar acciones de compensación.

  • Patrón de vista materializada. Este patrón crea vistas rellenadas previamente, conocidas como vistas materializadas, para realizar consultas eficaces y extraer datos de uno o varios almacenes de datos. El modelo de lectura de una implementación de CQRS puede contener vistas materializadas de los datos del modelo de escritura o el modelo de lectura se puede utilizar para generar vistas materializadas.