Command Query Responsibility Segregation (CQRS) è un modello di progettazione che separa le operazioni di lettura e scrittura per un archivio dati in modelli di dati separati. In questo modo ogni modello può essere ottimizzato in modo indipendente e può migliorare le prestazioni, la scalabilità e la sicurezza di un'applicazione.
Contesto e problema
Nelle architetture tradizionali, un singolo modello di dati viene spesso usato per le operazioni di lettura e scrittura. Questo approccio è semplice e funziona bene per le operazioni CRUD di base (vedere la figura 1).
Figura 1. Architettura CRUD tradizionale.
Tuttavia, man mano che le applicazioni aumentano, l'ottimizzazione delle operazioni di lettura e scrittura su un singolo modello di dati diventa sempre più complessa. Le operazioni di lettura e scrittura hanno spesso esigenze di prestazioni e scalabilità diverse. Un'architettura CRUD tradizionale non tiene conto di questi dati asimmetria. Comporta diverse sfide:
Mancata corrispondenza dei dati: Le rappresentazioni di lettura e scrittura dei dati spesso differiscono. Alcuni campi necessari durante gli aggiornamenti potrebbero non essere necessari durante le letture.
contesa di blocco: operazioni parallele sullo stesso set di dati possono causare conflitti di blocco.
Problemi di prestazioni: L'approccio tradizionale può influire negativamente sulle prestazioni a causa del carico sull'archivio dati e sul livello di accesso ai dati e sulla complessità delle query necessarie per recuperare le informazioni.
Problemi di sicurezza: La gestione della sicurezza diventa difficile quando le entità sono soggette a operazioni di lettura e scrittura. Questa sovrapposizione può esporre i dati in contesti imprevisti.
La combinazione di queste responsabilità può comportare un modello eccessivamente complicato che tenta di eseguire troppe operazioni.
Soluzione
Usare il modello CQRS per separare le operazioni di scrittura ( comandi) dalle operazioni di lettura (query). I comandi sono responsabili dell'aggiornamento dei dati. Le query sono responsabili del recupero dei dati.
Informazioni sui comandi. I comandi devono rappresentare attività aziendali specifiche anziché aggiornamenti di dati di basso livello. Ad esempio, in un'app di prenotazione di hotel, usa "Prenota camera hotel" anziché "Imposta ReservationStatus su Riservato". Questo approccio riflette meglio la finalità dietro le azioni dell'utente e allinea i comandi ai processi aziendali. Per assicurarsi che i comandi siano riusciti, potrebbe essere necessario perfezionare il flusso di interazione dell'utente, la logica lato server e prendere in considerazione l'elaborazione asincrona.
Area di perfezionamento | Raccomandazione |
---|---|
Convalida lato client | Convalidare determinate condizioni prima di inviare il comando per evitare errori evidenti. Ad esempio, se non sono disponibili camere, disabilitare il pulsante "Book" e fornire un messaggio chiaro e descrittivo nell'interfaccia utente che spiega perché la prenotazione non è possibile. Questa configurazione riduce le richieste server non necessarie e fornisce feedback immediato agli utenti, migliorando l'esperienza. |
Logica lato server | Migliorare la logica di business per gestire correttamente i casi perimetrali e gli errori. Ad esempio, per risolvere le race condition (più utenti che tentano di prenotare l'ultima sala disponibile), è consigliabile aggiungere utenti a una lista di attesa o suggerire opzioni alternative. |
Elaborazione asincrona | È anche possibile i comandi di processo in modo asincrono inserendoli in una coda, anziché gestirli in modo sincrono. |
Informazioni sulle query. Le query non modificano mai i dati. Restituiscono invece oggetti DTO (Data Transfer Objects) che presentano i dati necessari in un formato pratico, senza logica di dominio. Questa chiara separazione dei problemi semplifica la progettazione e l'implementazione del sistema.
Informazioni sulla separazione dei modelli di lettura e scrittura
La separazione del modello di lettura dal modello di scrittura semplifica la progettazione e l'implementazione del sistema risolvendo problemi distinti per le operazioni di scrittura e lettura dei dati. Questa separazione migliora chiarezza, scalabilità e prestazioni, ma introduce alcuni compromessi. Ad esempio, gli strumenti di scaffolding come i framework O/RM non possono generare automaticamente codice CQRS da uno schema di database, richiedendo logica personalizzata per colmare il divario.
Le sezioni seguenti illustrano due approcci principali per implementare la separazione dei modelli di lettura e scrittura in CQRS. Ogni approccio presenta vantaggi e sfide univoci, ad esempio la gestione della sincronizzazione e della coerenza.
Separazione dei modelli in un singolo archivio dati
Questo approccio rappresenta il livello di base di CQRS, in cui i modelli di lettura e scrittura condividono un singolo database sottostante ma mantengono una logica distinta per le operazioni. Definendo problematiche separate, questa strategia migliora la semplicità offrendo vantaggi in termini di scalabilità e prestazioni per i casi d'uso tipici. Un'architettura CQRS di base consente di delineare il modello di scrittura dal modello di lettura mentre si basa su un archivio dati condiviso (vedere la figura 2).
Figura 2. Architettura CQRS di base con un singolo archivio dati.
Questo approccio migliora chiarezza, prestazioni e scalabilità definendo modelli distinti per la gestione delle problematiche di scrittura e lettura:
Modello di scrittura: Progettato per gestire i comandi che aggiornano o salvano in modo permanente i dati. Include la convalida, la logica di dominio e garantisce la coerenza dei dati ottimizzando l'integrità transazionale e i processi aziendali.
Modello di lettura: progettato per gestire le query per il recupero dei dati. Si concentra sulla generazione di DTO (oggetti di trasferimento dati) o proiezioni ottimizzate per il livello di presentazione. Migliora le prestazioni delle query e la velocità di risposta evitando la logica di dominio.
Separazione fisica dei modelli in archivi dati separati
Un'implementazione più avanzata di CQRS usa archivi dati distinti per i modelli di lettura e scrittura. La separazione degli archivi dati di lettura e scrittura consente di ridimensionare ognuno in modo che corrisponda al carico. Consente inoltre di usare una tecnologia di archiviazione diversa per ogni archivio dati. È possibile usare un database di documenti per l'archivio dati di lettura e un database relazionale per l'archivio dati di scrittura (vedere la figura 3).
Figura 3. Architettura CQRS con archivi dati di lettura e scrittura separati.
Sincronizzazione di archivi dati separati: Quando si usano archivi separati, è necessario assicurarsi che entrambi rimangano sincronizzati. Un modello comune consiste nell'avere gli eventi di pubblicazione del modello di scrittura ogni volta che aggiorna il database, che il modello di lettura usa per aggiornare i dati. Per altre informazioni sull'uso degli eventi, vedere stile di architettura basata su eventi. Tuttavia, in genere non è possibile integrare broker e database di messaggi in una singola transazione distribuita. Pertanto, possono verificarsi problemi nella garanzia della coerenza durante l'aggiornamento del database e la pubblicazione di eventi. Per altre informazioni, vedere 'elaborazione di messaggi idempotenti.
Lettura archivio dati: L'archivio dati di lettura può usare il proprio schema di dati ottimizzato per le query. Ad esempio, può archiviare una vista materializzata dei dati per evitare join complessi o mapping O/RM. L'archivio di lettura può essere una replica di sola lettura dell'archivio di scrittura o avere una struttura diversa. La distribuzione di più repliche di sola lettura può migliorare le prestazioni riducendo la latenza e aumentando la disponibilità, soprattutto negli scenari distribuiti.
Vantaggi di CQRS
Scalabilità indipendente. CQRS consente la scalabilità indipendente dei modelli di lettura e scrittura, che consente di ridurre al minimo i conflitti di blocco e migliorare le prestazioni del sistema sotto carico.
Schemi di dati ottimizzati. Le operazioni di lettura possono usare uno schema ottimizzato per le query. Le operazioni di scrittura usano uno schema ottimizzato per gli aggiornamenti.
Protezione. Separando le operazioni di lettura e scrittura, è possibile assicurarsi che solo le entità o le operazioni di dominio appropriate dispongano dell'autorizzazione per eseguire azioni di scrittura sui dati.
Separazione delle attività. La suddivisione delle responsabilità di lettura e scrittura comporta modelli più puliti e gestibili. Il lato scrittura gestisce in genere una logica di business complessa, mentre il lato lettura può rimanere semplice e incentrato sull'efficienza delle query.
Query più semplici. Quando si archivia una vista materializzata nel database di lettura, l'applicazione può evitare join complessi durante l'esecuzione di query.
Problemi di implementazione e considerazioni
Alcune sfide dell'implementazione di questo modello includono:
Maggiore complessità. Anche se il concetto di base di CQRS è semplice, può introdurre una complessità significativa nella progettazione dell'applicazione, in particolare se combinata con il modello di origine eventi .
problemi di messaggistica. Anche se la messaggistica non è un requisito per CQRS, spesso viene usata per elaborare i comandi e pubblicare gli eventi di aggiornamento. Quando la messaggistica è coinvolta, il sistema deve tenere conto di potenziali problemi, ad esempio errori dei messaggi, duplicati e tentativi. Per le strategie per gestire i comandi con priorità diverse, vedere le linee guida sulle code di priorità .
Coerenza finale. Quando i database di lettura e scrittura sono separati, i dati di lettura potrebbero non riflettere immediatamente le modifiche più recenti, causando dati non aggiornati. Garantire che l'archivio modelli di lettura rimanga up-to-date con le modifiche nell'archivio modelli di scrittura può risultare complesso. Inoltre, il rilevamento e la gestione di scenari in cui un utente agisce su dati non aggiornati richiede un'attenta considerazione.
Quando usare il modello CQRS
Il modello CQRS è utile negli scenari che richiedono una netta separazione tra le modifiche ai dati (comandi) e le query sui dati (letture). Prendere in considerazione l'uso di CQRS nelle situazioni seguenti:
domini collaborativi: In ambienti in cui più utenti accedono e modificano contemporaneamente gli stessi dati, CQRS consente di ridurre i conflitti di unione. I comandi possono includere una granularità sufficiente per evitare conflitti e il sistema può risolvere qualsiasi condizione che si verifichi all'interno della logica del comando.
interfacce utente basate su attività: Applicazioni che guidano gli utenti attraverso processi complessi come una serie di passaggi o con modelli di dominio complessi traggono vantaggio da CQRS.
Il modello di scrittura ha uno stack completo di elaborazione dei comandi con logica di business, convalida dell'input e convalida aziendale. Il modello di scrittura può trattare un set di oggetti associati come una singola unità per le modifiche ai dati, noto come aggregato nella terminologia di progettazione basata su dominio. Il modello di scrittura potrebbe anche garantire che questi oggetti siano sempre in uno stato coerente.
Il modello di lettura non ha una logica di business o uno stack di convalida. Restituisce un oggetto DTO da usare in un modello di visualizzazione. Il modello di lettura è coerente con il modello di scrittura.
Ottimizzazione delle prestazioni: Sistemi in cui le prestazioni delle letture dei dati devono essere ottimizzate separatamente dalle prestazioni delle scritture di dati, soprattutto quando il numero di letture è maggiore del numero di scritture, trae vantaggio da CQRS. Il modello di lettura viene ridimensionato orizzontalmente per gestire volumi di query di grandi dimensioni, mentre il modello di scrittura viene eseguito su meno istanze per ridurre al minimo i conflitti di merge e mantenere la coerenza.
Separazione dei problemi di sviluppo: CQRS consente ai team di lavorare in modo indipendente. Un team si concentra sull'implementazione della logica di business complessa nel modello di scrittura, mentre un altro sviluppa i componenti del modello di lettura e dell'interfaccia utente.
sistemi in evoluzione: CQRS supporta sistemi che si evolvono nel tempo. Supporta nuove versioni del modello, modifiche frequenti alle regole business o altre modifiche senza influire sulle funzionalità esistenti.
'integrazione del sistema: sistemi che si integrano con altri sottosistemi, in particolare quelli che usano Event Sourcing, rimangono disponibili anche se un sottosistema ha esito negativo temporaneamente. CQRS isola gli errori, impedendo a un singolo componente di influire sull'intero sistema.
Quando non usare CQRS
Evitare CQRS nelle situazioni seguenti:
Il dominio o le regole business sono semplici.
Un'interfaccia utente di tipo CRUD semplice e le operazioni di accesso ai dati sono sufficienti.
Progettazione del carico di lavoro
Un architetto deve valutare come usare il modello CQRS nella progettazione del carico di lavoro per soddisfare gli obiettivi e i principi trattati nei pilastri di Azure Well-Architected Framework . Ad esempio:
Concetto fondamentale | Come questo modello supporta gli obiettivi di pilastro |
---|---|
L'efficienza delle prestazioni consente al carico di lavoro di soddisfare in modo efficiente le richieste tramite ottimizzazioni in termini di scalabilità, dati, codice. | La separazione delle operazioni di lettura e scrittura in carichi di lavoro con operazioni di lettura/scrittura elevate consente di ottimizzare le prestazioni e la scalabilità mirate per lo scopo specifico di ogni operazione. - PE:05 Ridimensionamento e partizionamento - Prestazioni dei dati PE:08 |
Come per qualsiasi decisione di progettazione, prendere in considerazione eventuali compromessi rispetto agli obiettivi degli altri pilastri che potrebbero essere introdotti con questo modello.
Combinazione di origine eventi e CQRS
Alcune implementazioni di CQRS incorporano il modello di origine eventi , che archivia lo stato del sistema come una serie cronologica di eventi. Ogni evento acquisisce le modifiche apportate ai dati in un determinato momento. Per determinare lo stato corrente, il sistema riproduce questi eventi in ordine. In questa combinazione:
L'archivio eventi è il modello di scrittura e l'unica fonte di verità.
Il modello letto genera viste materializzate da questi eventi, in genere in un formato altamente denormalizzato. Queste viste ottimizzano il recupero dei dati personalizzando le strutture per eseguire query e visualizzare i requisiti.
Vantaggi della combinazione di origine eventi e CQRS
Gli stessi eventi che aggiornano il modello di scrittura possono fungere da input per il modello di lettura. Il modello di lettura può quindi creare uno snapshot in tempo reale dello stato corrente. Questi snapshot ottimizzano le query fornendo visualizzazioni pre-calcolate efficienti dei dati.
Anziché archiviare direttamente lo stato corrente, il sistema usa un flusso di eventi come archivio di scrittura. Questo approccio riduce i conflitti di aggiornamento sulle aggregazioni e migliora le prestazioni e la scalabilità. Il sistema può elaborare questi eventi in modo asincrono per compilare o aggiornare le viste materializzate per l'archivio di lettura.
Poiché l'archivio eventi funge da singola fonte di verità, è possibile rigenerare facilmente le visualizzazioni materializzate o adattarsi alle modifiche nel modello di lettura riproducendo gli eventi cronologici. In sostanza, le viste materializzate funzionano come cache durevole e di sola lettura ottimizzate per query veloci ed efficienti.
Considerazioni sulla combinazione di origine eventi e CQRS
Prima di combinare il modello CQRS con il modello di origine eventi , valutare le considerazioni seguenti:
Coerenza finale: Poiché gli archivi di scrittura e lettura sono separati, gli aggiornamenti all'archivio di lettura potrebbero essere in ritardo rispetto alla generazione di eventi, con conseguente coerenza finale.
maggiore complessità: combinare CQRS con Event Sourcing richiede un approccio di progettazione diverso, che può rendere più complessa l'implementazione di successo. È necessario scrivere codice per generare, elaborare e gestire gli eventi e assemblare o aggiornare le viste per il modello di lettura. Tuttavia, l'origine eventi semplifica la modellazione del dominio e consente di ricompilare o creare nuove visualizzazioni facilmente conservando la cronologia e la finalità di tutte le modifiche ai dati.
Prestazioni della generazione di viste: Generazione di viste materializzate per il modello di lettura può richiedere tempo e risorse significative. Lo stesso vale per proiettare i dati riproducendo ed elaborando eventi per entità o raccolte specifiche. Questo effetto aumenta quando i calcoli comportano l'analisi o la somma dei valori in lunghi periodi, in quanto tutti gli eventi correlati devono essere esaminati. Implementare snapshot dei dati a intervalli regolari. Ad esempio, archiviare snapshot periodici di totali aggregati (il numero di volte in cui si verifica un'azione specifica) o lo stato corrente di un'entità. Gli snapshot riducono la necessità di elaborare ripetutamente la cronologia eventi completa, migliorando le prestazioni.
Esempio di modello CQRS
Il codice seguente mostra alcuni estratti da un esempio di un'implementazione del modello CQRS che usa definizioni diverse per modelli di lettura e di scrittura. Le interfacce di modello non impongono alcuna funzionalità degli archivi dati sottostanti e possono evolversi ed essere ottimizzate in modo indipendente, perché queste interfacce sono separate.
Il codice seguente illustra la definizione di modello di lettura.
// 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; }
}
}
Il sistema consente agli utenti di valutare i prodotti. Il codice dell'applicazione può usare il comando RateProduct
illustrato nel codice seguente.
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; }
}
Il sistema usa la classe ProductsCommandHandler
per gestire i comandi inviati dall'applicazione. I client inviano in genere comandi al dominio usando un sistema di messaggistica, ad esempio una coda. Il gestore del comando accetta tali comandi e richiama i metodi dell'interfaccia di dominio. La granularità di ogni comando è progettata per ridurre il rischio di richieste in conflitto. Il codice seguente illustra la 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)
{
...
}
}
Passaggi successivi
Quando si implementa questo modello, possono essere utili i modelli e le linee guida seguenti:
- Partizionamento orizzontale, verticale e funzionale dei dati. Vengono descritte le procedure consigliate per la suddivisione dei dati in partizioni a cui è possibile accedere separatamente per migliorare la scalabilità, ridurre i conflitti e ottimizzare le prestazioni.
Risorse correlate
Modello di origine eventi. Viene descritto come usare l'origine eventi con il modello CQRS. Illustra come semplificare le attività in domini complessi migliorando prestazioni, scalabilità e velocità di risposta. Viene inoltre illustrato come garantire la coerenza per i dati transazionali mantenendo al tempo tempo pieno audit trail e cronologia che possono abilitare azioni di compensazione.
Modello di vista materializzata. Il modello di lettura di un'implementazione CQRS può contenere viste materializzate dei dati del modello di scrittura oppure può essere usato per generare viste materializzate.