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. Esto 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 las arquitecturas tradicionales, a menudo se usa un único modelo de datos para las operaciones de lectura y escritura. Este enfoque es sencillo y funciona bien para las operaciones CRUD básicas (ver la figura 1).
Figura 1. Una arquitectura CRUD tradicional.
Sin embargo, a medida que las aplicaciones crecen, la optimización de las operaciones de lectura y escritura en un único modelo de datos se vuelve cada vez más difícil. Las operaciones de lectura y escritura suelen tener diferentes necesidades de rendimiento y escalado. Una arquitectura CRUD tradicional no tiene en cuenta esta asimetría. Conduce a varios 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 lecturas.
Contención de bloqueo: operaciones paralelas en el mismo conjunto de datos puede provocar contención de bloqueos.
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.
Problemas de seguridad: La administración de la seguridad se vuelve difícil 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 que intenta hacer demasiado.
Solución
Use el patrón CQRS para separar las operaciones de escritura ( comandos de) de las operaciones de lectura (consultas). Los comandos son responsables de actualizar los datos. Las consultas son responsables de recuperar datos.
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 "Reservar habitación de hotel" en lugar de "Establecer ReservationStatus en Reservado". Este enfoque refleja mejor la intención detrás de las acciones del usuario y alinea los comandos con los procesos empresariales. Para asegurarse de que los comandos son correctos, es posible que tenga que refinar el flujo de interacción del usuario, la lógica del lado servidor y considerar el procesamiento asincrónico.
Área de refinamiento | Recomendación |
---|---|
Validación del lado cliente | Valide ciertas condiciones antes de enviar el comando para evitar errores obvios. Por ejemplo, si no hay salas 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 carrera (varios usuarios que intentan reservar la última sala disponible), considere la posibilidad de agregar usuarios a una lista de espera o sugerir opciones alternativas. |
Procesamiento asincrónico | También puede comandos de proceso de forma asincrónica colocandolos 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 clara separación de preocupaciones simplifica el diseño y la implementación del sistema.
Descripción de la separación de modelos de lectura y 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 distintos para las escrituras y lecturas de datos. Esta separación mejora la claridad, la escalabilidad y el rendimiento, pero presenta algunas ventajas. Por ejemplo, las herramientas de scaffolding como marcos de O/RM no pueden generar automáticamente código CQRS a partir de un esquema de base de datos, lo que requiere lógica personalizada para cerrar la brecha.
En las secciones siguientes se exploran dos enfoques principales para implementar la separación de modelos de lectura y escritura en CQRS. Cada enfoque incluye ventajas y desafíos únicos, como la administración de sincronización y coherencia.
Separación de 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. Al definir preocupaciones independientes, esta estrategia mejora la simplicidad a la vez que ofrece ventajas en la escalabilidad y el rendimiento para casos de uso típicos. 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 (ver la figura 2).
Figura 2. Una arquitectura básica de CQRS con un único almacén de datos.
Este enfoque mejora la claridad, el rendimiento y la escalabilidad definiendo modelos distintos para controlar los problemas de escritura y lectura:
Escribir modelo: Diseñado para controlar comandos que actualizan o conservan datos. Incluye validación, lógica de dominio y garantiza la coherencia de los datos mediante la optimización de la integridad transaccional y los procesos empresariales.
modelo de lectura: Diseñado para atender consultas para recuperar datos. Se centra en generar DTO (objetos de transferencia de datos) 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.
Separación física de modelos en almacenes de datos independientes
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 una 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 (vea la figura 3).
Figura 3. Una arquitectura CQRS con almacenes de datos de lectura y escritura independientes.
Sincronización de almacenes de datos independientes: Al usar almacenes independientes, debe asegurarse de que ambos permanecen sincronizados. Un patrón común es hacer que el modelo de escritura publique eventos cada vez que actualiza la base de datos, que el modelo de lectura usa para actualizar sus datos. Para obtener más información sobre el uso de eventos, consulte estilo de arquitectura controlada por eventos. Sin embargo, normalmente no se pueden inscribir agentes de mensajes y bases de datos en una sola transacción distribuida. Por lo tanto, puede haber desafíos para garantizar la coherencia al actualizar la base de datos y publicar eventos. Para obtener más información, consulte procesamiento de mensajes idempotentes.
Almacén de datos de lectura: 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 combinaciones complejas o asignaciones de O/RM. El almacén 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, lo que 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 cuestiones. Dividir 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, mientras que 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 al consultar.
Consideraciones y problemas de implementación
Estos son algunos de los desafíos de la implementación de este patrón:
Mayor complejidad. Aunque el concepto básico de CQRS es sencillo, puede introducir una complejidad significativa en el diseño de la aplicación, especialmente cuando se combina con el patrón de origen de eventos .
desafíos de mensajería. Aunque la mensajería no es un requisito para CQRS, a menudo se usa para procesar comandos y publicar eventos de actualización. Cuando la mensajería está implicada, el sistema debe tener en cuenta posibles problemas, como errores de mensaje, duplicados y reintentos. Consulte las instrucciones sobre colas de prioridad para ver las estrategias para controlar los comandos con distintas prioridades.
Coherencia final. Cuando las bases de datos de lectura y escritura están separadas, es posible que los datos de lectura no reflejen los cambios más recientes inmediatamente, lo que conduce a datos obsoletos. Asegurarse de que el almacén de modelos de lectura permanece up-to-date con los cambios en el almacén de modelos de escritura puede ser difícil. Además, la detección y el control de escenarios en los que un usuario actúa sobre datos obsoletos requiere una consideración cuidadosa.
Cuándo usar el patrón CQRS
El patrón CQRS es útil en escenarios que requieren una separación clara entre modificaciones de datos (comandos) y consultas de datos (lecturas). Considere la posibilidad de usar CQRS en las situaciones siguientes:
dominios colaborativos: En entornos donde 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 cualquier problema que surja dentro de la lógica de comandos.
interfaces de usuario basadas en tareas: 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, conocido como un agregado en terminología de diseño controlado por dominio. El modelo de escritura también puede asegurarse de que estos objetos siempre están 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.
Ajuste del rendimiento: Sistemas en los que el rendimiento de las lecturas de datos se debe ajustar independientemente del rendimiento de las escrituras de datos, especialmente cuando el número de lecturas es mayor que el número de escrituras, beneficiarse de CQRS. El modelo de lectura se escala horizontalmente para controlar grandes volúmenes de consultas, mientras que el modelo de escritura se ejecuta en menos instancias para minimizar los conflictos de combinación y mantener la coherencia.
Separación de problemas de desarrollo: CQRS permite a los equipos trabajar de forma independiente. Un equipo se centra en implementar la lógica de negocios compleja en el modelo de escritura, mientras que otro desarrolla los componentes de la interfaz de usuario y el modelo de lectura.
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.
Integración del sistema: Sistemas que se integran con otros subsistemas, especialmente aquellos que usan Event Sourcing, permanecen 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.
Cuándo no usar CQRS
Evite CQRS en las situaciones siguientes:
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
Un arquitecto debe evaluar cómo usar el patrón CQRS en el diseño de su carga de trabajo para abordar los objetivos y principios descritos en los pilares de Azure Well-Architected Framework. Por ejemplo:
Fundamento | Cómo apoya este patrón los objetivos de los pilares |
---|---|
La eficiencia del rendimiento ayuda a que la carga de trabajo satisfaga eficazmente las demandas mediante optimizaciones en el escalado, los datos y el código. | La separación de las operaciones de lectura y escritura en las cargas de trabajo de lectura-escritura intensiva permite optimizar el rendimiento y escalado específicos para el propósito concreto de cada operación. - PE:05 Escapado y particiones - PE:08 Rendimiento de datos |
Al igual que con cualquier decisión de diseño, hay que tener en cuenta las ventajas y desventajas con respecto a los objetivos de los otros pilares que podrían introducirse con este patrón.
Combinación de orígenes de eventos y CQRS
Algunas implementaciones de CQRS incorporan el patrón de Event Sourcing, que almacena el estado del sistema como una serie cronológica de eventos. Cada evento captura los cambios realizados en los datos en un momento dado. Para determinar el estado actual, el sistema reproduce estos eventos en orden. En esta combinación:
El almacén de eventos es el modelo de escritura y la única fuente de 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 el origen de eventos 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 precaladas 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 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. En esencia, las vistas materializadas funcionan como una memoria caché duradera y de solo lectura optimizada para consultas rápidas y eficaces.
Consideraciones al combinar el origen de eventos 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 escritura y lectura son independientes, las actualizaciones del almacén de lectura podrían retardar la generación de eventos, lo que da lugar a una coherencia final.
Mayor complejidad: Combinar CQRS con Event Sourcing requiere un enfoque de diseño diferente, lo que puede hacer que la implementación 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, Event Sourcing simplifica el modelado de dominios 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. Este efecto aumenta cuando los cálculos implican analizar o sumar valores durante largos períodos, ya que se deben examinar todos los eventos relacionados. Implemente instantáneas de los datos a intervalos regulares. Por ejemplo, almacene instantáneas periódicas de totales agregados (el número de veces que se produce una acción específica) o el estado actual de una entidad. Las instantáneas reducen la necesidad de procesar repetidamente el historial de eventos completo, lo que mejora el rendimiento.
Ejemplo de patrón CQRS
El código siguiente muestra algunos extractos de un ejemplo de una implementación CQRS que usa definiciones diferentes para los modelos de lectura y de escritura. Las interfaces de los modelos no dictan ninguna característica de los almacenes de datos subyacentes, y pueden evolucionar y adaptarse de forma independiente ya que estas interfaces están separadas.
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 hace esto mediante el comando RateProduct
que aparece 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 clase ProductsCommandHandler
para controlar los comandos enviados por 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)
{
...
}
}
Pasos siguientes
Los patrones y las directrices siguientes son útiles a la hora de implementar este patrón:
- Creación de particiones de datos horizontales, verticales y funcionales Describe los procedimientos recomendados para dividir los datos en particiones que se pueden administrar y a las que se tiene acceso por separado para mejorar la escalabilidad, reducir la contención y optimizar el rendimiento.
Recursos relacionados
Patrón Event Sourcing. Describe cómo usar Event Sourcing con el patrón CQRS. Muestra cómo simplificar las tareas en dominios complejos a la vez que mejora 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 Materialized View. 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.