Command Query Responsibility Segregation (CQRS) is een ontwerppatroon waarmee lees- en schrijfbewerkingen voor een gegevensarchief worden gescheiden in afzonderlijke gegevensmodellen. Hierdoor kan elk model onafhankelijk worden geoptimaliseerd en kunnen de prestaties, schaalbaarheid en beveiliging van een toepassing worden verbeterd.
Context en probleem
In traditionele architecturen wordt één gegevensmodel vaak gebruikt voor zowel lees- als schrijfbewerkingen. Deze benadering is eenvoudig en werkt goed voor eenvoudige CRUD-bewerkingen (zie afbeelding 1).
Afbeelding 1. Een traditionele CRUD-architectuur.
Naarmate toepassingen groeien, wordt het optimaliseren van lees- en schrijfbewerkingen op één gegevensmodel echter steeds moeilijker. Lees- en schrijfbewerkingen hebben vaak verschillende prestatie- en schaalbehoeften. Een traditionele CRUD-architectuur houdt geen rekening met deze asymmetrie. Dit leidt tot verschillende uitdagingen:
gegevens komen niet overeen: De lees- en schrijfweergaven van gegevens verschillen vaak. Sommige velden die tijdens updates zijn vereist, zijn mogelijk niet nodig tijdens leesbewerkingen.
Conflicten vergrendelen: parallelle bewerkingen in dezelfde gegevensset kunnen leiden tot vergrendelingsconflicten.
Prestatieproblemen: De traditionele benadering kan een negatief effect hebben op de prestaties vanwege de belasting van het gegevensarchief en de laag voor gegevenstoegang, en de complexiteit van query's die nodig zijn om informatie op te halen.
Beveiligingsproblemen: Het beheren van beveiliging wordt moeilijk wanneer entiteiten onderhevig zijn aan lees- en schrijfbewerkingen. Deze overlapping kan gegevens in onbedoelde contexten beschikbaar maken.
Het combineren van deze verantwoordelijkheden kan leiden tot een te ingewikkeld model dat te veel probeert te doen.
Oplossing
Gebruik het CQRS-patroon om schrijfbewerkingen (opdrachten) te scheiden van leesbewerkingen (query's). Opdrachten zijn verantwoordelijk voor het bijwerken van gegevens. Query's zijn verantwoordelijk voor het ophalen van gegevens.
Opdrachten begrijpen. Opdrachten moeten specifieke zakelijke taken vertegenwoordigen in plaats van gegevensupdates op laag niveau. Gebruik in een hotelreserverings-app bijvoorbeeld 'Hotelkamer boeken' in plaats van 'Set ReservationStatus to Reserved'. Deze benadering weerspiegelt de intentie achter gebruikersacties en lijnt opdrachten af met bedrijfsprocessen. Om ervoor te zorgen dat opdrachten zijn geslaagd, moet u mogelijk de gebruikersinteractiestroom, logica aan de serverzijde verfijnen en asynchrone verwerking overwegen.
Verfijningsgebied | Aanbeveling |
---|---|
Validatie aan clientzijde | Valideer bepaalde voorwaarden voordat u de opdracht verzendt om duidelijke fouten te voorkomen. Als er bijvoorbeeld geen ruimten beschikbaar zijn, schakelt u de knop Boek uit en geeft u een duidelijk, gebruiksvriendelijk bericht in de gebruikersinterface waarin wordt uitgelegd waarom boeken niet mogelijk is. Deze installatie vermindert onnodige serveraanvragen en biedt onmiddellijke feedback aan gebruikers, waardoor hun ervaring wordt verbeterd. |
Logica aan serverzijde | Verbeter de bedrijfslogica om edge-aanvragen en fouten probleemloos af te handelen. Als u bijvoorbeeld racevoorwaarden wilt aanpakken (meerdere gebruikers die de laatste beschikbare ruimte willen boeken), kunt u overwegen om gebruikers toe te voegen aan een wachtlijst of alternatieve opties te voorstellen. |
Asynchrone verwerking | U kunt opdrachten ook asynchroon |
Query's begrijpen. Query's veranderen nooit gegevens. In plaats daarvan retourneren ze DTU's (Data Transfer Objects) die de vereiste gegevens in een handige indeling presenteren, zonder domeinlogica. Deze duidelijke scheiding van zorgen vereenvoudigt het ontwerp en de implementatie van het systeem.
Lees- en schrijfmodelscheiding begrijpen
Het scheiden van het leesmodel van het schrijfmodel vereenvoudigt het ontwerpen en implementeren van het systeem door verschillende zorgen voor gegevensschrijf- en leesbewerkingen aan te pakken. Deze scheiding verbetert de duidelijkheid, schaalbaarheid en prestaties, maar introduceert enkele compromissen. Met hulpprogramma's zoals O/RM-frameworks kan bijvoorbeeld niet automatisch CQRS-code worden gegenereerd op basis van een databaseschema, waarvoor aangepaste logica nodig is voor het overbruggen van de kloof.
In de volgende secties worden twee primaire benaderingen beschreven voor het implementeren van scheiding van lees- en schrijfmodellen in CQRS. Elke benadering heeft unieke voordelen en uitdagingen, zoals synchronisatie- en consistentiebeheer.
Scheiding van modellen in één gegevensarchief
Deze benadering vertegenwoordigt het fundamentele niveau van CQRS, waarbij zowel de lees- als schrijfmodellen één onderliggende database delen, maar afzonderlijke logica voor hun bewerkingen behouden. Door afzonderlijke problemen te definiëren, verbetert deze strategie de eenvoud en levert deze voordelen op het gebied van schaalbaarheid en prestaties voor typische gebruiksvoorbeelden. Met een eenvoudige CQRS-architectuur kunt u het schrijfmodel uit het leesmodel afbakenen terwijl u afhankelijk bent van een gedeeld gegevensarchief (zie afbeelding 2).
Afbeelding 2. Een eenvoudige CQRS-architectuur met één gegevensarchief.
Deze aanpak verbetert de duidelijkheid, prestaties en schaalbaarheid door afzonderlijke modellen te definiëren voor het afhandelen van schrijf- en leesproblemen:
Model schrijven: Ontworpen voor het verwerken van opdrachten die gegevens bijwerken of behouden. Het omvat validatie, domeinlogica en zorgt voor gegevensconsistentie door te optimaliseren voor transactionele integriteit en bedrijfsprocessen.
Leesmodel: Ontworpen om query's te leveren voor het ophalen van gegevens. Het richt zich op het genereren van DTU's (gegevensoverdrachtobjecten) of projecties die zijn geoptimaliseerd voor de presentatielaag. Het verbetert de prestaties en reactiesnelheid van query's door domeinlogica te vermijden.
Fysieke scheiding van modellen in afzonderlijke gegevensarchieven
Een geavanceerdere CQRS-implementatie maakt gebruik van afzonderlijke gegevensarchieven voor de lees- en schrijfmodellen. Door de scheiding van de lees- en schrijfgegevensarchieven kunt u deze schalen zodat deze overeenkomen met de belasting. Hiermee kunt u ook een andere opslagtechnologie gebruiken voor elk gegevensarchief. U kunt een documentdatabase gebruiken voor het leesgegevensarchief en een relationele database voor het gegevensarchief schrijven (zie afbeelding 3).
Afbeelding 3. Een CQRS-architectuur met afzonderlijke gegevensarchieven voor lezen en schrijven.
Afzonderlijke gegevensarchieven synchroniseren: Wanneer u afzonderlijke archieven gebruikt, moet u ervoor zorgen dat beide gesynchroniseerd blijven. Een veelvoorkomend patroon is dat het schrijfmodel gebeurtenissen publiceert wanneer de database wordt bijgewerkt, die door het leesmodel wordt gebruikt om de gegevens te vernieuwen. Zie architectuurstijl op basis van gebeurtenissenvoor meer informatie over het gebruik van gebeurtenissen. Meestal kunt u echter geen berichtenbrokers en -databases in een enkele gedistribueerde transactie insluiten. Er kunnen dus uitdagingen zijn bij het garanderen van consistentie bij het bijwerken van de database en het publiceren van gebeurtenissen. Zie idempotent berichtverwerkingvoor meer informatie.
Gegevensarchief lezen: Het leesgegevensarchief kan een eigen gegevensschema gebruiken dat is geoptimaliseerd voor query's. Het kan bijvoorbeeld een gerealiseerde weergave opslaan van de gegevens om complexe joins of O/RM-toewijzingen te voorkomen. Het leesarchief kan een alleen-lezen replica van het schrijfarchief zijn of een andere structuur hebben. Het implementeren van meerdere alleen-lezen replica's kan de prestaties verbeteren door latentie te verminderen en de beschikbaarheid te vergroten, met name in gedistribueerde scenario's.
Voordelen van CQRS
Onafhankelijk schalen. Met CQRS kunnen de lees- en schrijfmodellen onafhankelijk worden geschaald, wat kan helpen bij het minimaliseren van conflicten over vergrendelingen en het verbeteren van de systeemprestaties onder belasting.
Geoptimaliseerde gegevensschema's. Leesbewerkingen kunnen een schema gebruiken dat is geoptimaliseerd voor query's. Schrijfbewerkingen maken gebruik van een schema dat is geoptimaliseerd voor updates.
Beveiliging. Door lees- en schrijfbewerkingen te scheiden, kunt u ervoor zorgen dat alleen de juiste domeinentiteiten of bewerkingen gemachtigd zijn om schrijfacties uit te voeren op de gegevens.
Scheiding van taken. Het splitsen van de verantwoordelijkheden voor lezen en schrijven resulteert in schonere, beter onderhoudbare modellen. De schrijfzijde verwerkt doorgaans complexe bedrijfslogica, terwijl de leeszijde eenvoudig en gericht kan blijven op de efficiëntie van query's.
Eenvoudigere query's. Wanneer u een gerealiseerde weergave opslaat in de leesdatabase, kan de toepassing complexe joins voorkomen bij het uitvoeren van query's.
Implementatieproblemen en overwegingen
Enkele uitdagingen bij het implementeren van dit patroon zijn:
Verhoogde complexiteit. Hoewel het kernconcept van CQRS eenvoudig is, kan het een aanzienlijke complexiteit in het toepassingsontwerp veroorzaken, met name in combinatie met het patroon Gebeurtenisbronnen.
Messaging-uitdagingen. Hoewel berichten niet vereist zijn voor CQRS, gebruikt u deze vaak om opdrachten te verwerken en update-gebeurtenissen te publiceren. Wanneer er berichten worden verzonden, moet het systeem rekening houden met mogelijke problemen, zoals berichtfouten, duplicaten en nieuwe pogingen. Zie de richtlijnen voor Prioriteitswachtrijen voor strategieën voor het afhandelen van opdrachten met verschillende prioriteiten.
Uiteindelijke consistentie. Wanneer de lees- en schrijfdatabases worden gescheiden, worden de leesgegevens mogelijk niet direct weergegeven met de meest recente wijzigingen, wat leidt tot verouderde gegevens. Het kan lastig zijn om ervoor te zorgen dat het leesmodelarchief up-to-date blijft met wijzigingen in het schrijfmodelarchief. Daarnaast is zorgvuldige overweging vereist bij het detecteren en verwerken van scenario's waarbij een gebruiker op verouderde gegevens reageert.
Wanneer gebruikt u het CQRS-patroon
Het CQRS-patroon is handig in scenario's waarvoor een duidelijke scheiding tussen gegevenswijzigingen (opdrachten) en gegevensquery's (leesbewerkingen) is vereist. Overweeg het gebruik van CQRS in de volgende situaties:
Collaborative-domeinen: In omgevingen waar meerdere gebruikers dezelfde gegevens tegelijk openen en wijzigen, helpt CQRS samenvoegingsconflicten te verminderen. Opdrachten kunnen voldoende granulariteit bevatten om conflicten te voorkomen en het systeem kan elk probleem oplossen dat zich in de opdrachtlogica voordoet.
gebruikersinterfaces op basis van taken: toepassingen die gebruikers leiden door complexe processen als een reeks stappen of met complexe domeinmodellen profiteren van CQRS.
Het schrijfmodel heeft een volledige stack voor het verwerken van opdrachten met bedrijfslogica, invoervalidatie en bedrijfsvalidatie. Het schrijfmodel kan een set gekoppelde objecten behandelen als één eenheid voor gegevenswijzigingen, bekend als een geaggregeerde in domeingestuurde ontwerpterminologie. Het schrijfmodel kan er ook voor zorgen dat deze objecten altijd een consistente status hebben.
Het leesmodel heeft geen bedrijfslogica of validatiestack. Het retourneert een DTO voor gebruik in een weergavemodel. Het leesmodel is uiteindelijk consistent met het schrijfmodel.
Prestaties afstemmen: Systemen waarbij de prestaties van gegevensleesbewerkingen afzonderlijk moeten worden afgestemd op de prestaties van gegevensschrijfbewerkingen, met name wanneer het aantal leesbewerkingen groter is dan het aantal schrijfbewerkingen, profiteert u van CQRS. Het leesmodel wordt horizontaal geschaald om grote queryvolumes te verwerken, terwijl het schrijfmodel wordt uitgevoerd op minder exemplaren om samenvoegingsconflicten te minimaliseren en consistentie te behouden.
Scheiding van ontwikkelingsproblemen: CQRS stelt teams in staat onafhankelijk te werken. Eén team richt zich op het implementeren van de complexe bedrijfslogica in het schrijfmodel, terwijl een andere het leesmodel en de onderdelen van de gebruikersinterface ontwikkelt.
Ontwikkelende systemen: CQRS ondersteunt systemen die zich in de loop van de tijd ontwikkelen. Het biedt plaats aan nieuwe modelversies, frequente wijzigingen in bedrijfsregels of andere wijzigingen zonder dat dit van invloed is op de bestaande functionaliteit.
Systeemintegratie: Systemen die kunnen worden geïntegreerd met andere subsystemen, met name systemen die gebruikmaken van Gebeurtenisbronnen, blijven beschikbaar, zelfs als een subsysteem tijdelijk uitvalt. CQRS isoleert fouten, waardoor één onderdeel het hele systeem niet beïnvloedt.
Wanneer u CQRS niet gebruikt
Vermijd CQRS in de volgende situaties:
Het domein of de bedrijfsregels zijn eenvoudig.
Een eenvoudige CRUD-gebruikersinterface en gegevenstoegangsbewerkingen zijn voldoende.
Workloadontwerp
Een architect moet evalueren hoe het CQRS-patroon in het ontwerp van de workload moet worden gebruikt om de doelstellingen en principes te verhelpen die worden behandeld in de pijlers van Azure Well-Architected Framework. Voorbeeld:
Pijler | Hoe dit patroon ondersteuning biedt voor pijlerdoelen |
---|---|
Prestatie-efficiëntie helpt uw workload efficiënt te voldoen aan de vereisten door optimalisaties in schalen, gegevens, code. | De scheiding van lees- en schrijfbewerkingen in workloads met hoge lees-naar-schrijfbewerkingen maakt gerichte prestaties en schaaloptimalisaties mogelijk voor het specifieke doel van elke bewerking. - PE:05 Schalen en partitioneren - PE:08 Gegevensprestaties |
Net als bij elke ontwerpbeslissing moet u rekening houden met eventuele compromissen ten opzichte van de doelstellingen van de andere pijlers die met dit patroon kunnen worden geïntroduceerd.
Gebeurtenisbronnen en CQRS combineren
Sommige implementaties van CQRS bevatten het patroon Gebeurtenisbronnen, waarin de status van het systeem wordt opgeslagen als een chronologische reeks gebeurtenissen. Elke gebeurtenis legt de wijzigingen vast die zijn aangebracht in de gegevens op een bepaald moment. Om de huidige status te bepalen, worden deze gebeurtenissen opnieuw afgespeeld in de volgorde van het systeem. In deze combinatie:
Het gebeurtenisarchief is het -schrijfmodel en de enige bron van waarheid.
Het model lezen gerealiseerde weergaven van deze gebeurtenissen genereert, meestal in een sterk gedenormaliseerde vorm. Deze weergaven optimaliseren het ophalen van gegevens door structuren aan te passen aan query- en weergavevereisten.
Voordelen van het combineren van gebeurtenisbronnen en CQRS
Dezelfde gebeurtenissen die het schrijfmodel bijwerken, kunnen fungeren als invoer voor het leesmodel. Het leesmodel kan vervolgens een realtime momentopname van de huidige status maken. Deze momentopnamen optimaliseren query's door efficiënte, vooraf samengestelde weergaven van de gegevens te bieden.
In plaats van de huidige status rechtstreeks op te slaan, gebruikt het systeem een stroom gebeurtenissen als het schrijfarchief. Deze aanpak vermindert updateconflicten op aggregaties en verbetert de prestaties en schaalbaarheid. Het systeem kan deze gebeurtenissen asynchroon verwerken om gerealiseerde weergaven voor het leesarchief te bouwen of bij te werken.
Omdat het gebeurtenisarchief fungeert als de enige bron van waarheid, kunt u eenvoudig gerealiseerde weergaven opnieuw genereren of zich aanpassen aan wijzigingen in het leesmodel door historische gebeurtenissen opnieuw af te spelen. In wezen werken gerealiseerde weergaven als een duurzame, alleen-lezen cache die is geoptimaliseerd voor snelle en efficiënte query's.
Overwegingen bij het combineren van gebeurtenisbronnen en CQRS
Voordat u het CQRS-patroon combineert met het patroon gebeurtenisbronnen, moet u de volgende overwegingen evalueren:
uiteindelijke consistentie: Omdat de schrijf- en leesarchieven gescheiden zijn, kunnen updates van het leesarchief achterblijven bij het genereren van gebeurtenissen, wat resulteert in uiteindelijke consistentie.
Verhoogde complexiteit: het combineren van CQRS met gebeurtenisbronnen vereist een andere ontwerpbenadering, waardoor een succesvolle implementatie lastiger kan worden. U moet code schrijven om gebeurtenissen te genereren, te verwerken en te verwerken en weergaven voor het leesmodel samen te stellen of bij te werken. Event Sourcing vereenvoudigt het modelleren van domeinen echter en stelt u in staat om eenvoudig nieuwe weergaven opnieuw te bouwen of te maken door de geschiedenis en intentie van alle gegevenswijzigingen te behouden.
Prestaties van het genereren van weergaven: Gerealiseerde weergaven genereren voor het leesmodel kan aanzienlijke tijd en resources verbruiken. Hetzelfde geldt voor het projecteren van gegevens door gebeurtenissen opnieuw af te spelen en te verwerken voor specifieke entiteiten of verzamelingen. Dit effect neemt toe wanneer berekeningen betrekking hebben op het analyseren of optellen van waarden gedurende lange perioden, omdat alle gerelateerde gebeurtenissen moeten worden onderzocht. Implementeer momentopnamen van de gegevens met regelmatige tussenpozen. Sla bijvoorbeeld periodieke momentopnamen op van geaggregeerde totalen (het aantal keren dat een specifieke actie plaatsvindt) of de huidige status van een entiteit. Momentopnamen verminderen de noodzaak om de volledige gebeurtenisgeschiedenis herhaaldelijk te verwerken, waardoor de prestaties worden verbeterd.
Voorbeeld van CQRS-patroon
De volgende code bestaat uit enkele extracten van een voorbeeld van een CQRS-implementatie waarin verschillende definities worden gebruikt voor de lees- en schrijfmodellen. De modelinterfaces bepalen geen functies van de onderliggende gegevensarchieven, en ze kunnen onafhankelijk van elkaar worden uitontwikkeld en bijgesteld omdat de interfaces zijn gescheiden.
De volgende code toont de definitie van het leesmodel.
// 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; }
}
}
Het systeem biedt gebruikers de mogelijkheid om producten te beoordelen. De toepassingscode doet dit met behulp van de opdracht RateProduct
in de volgende code.
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; }
}
Het systeem gebruikt de klasse ProductsCommandHandler
voor het verwerken van opdrachten die door de toepassing zijn verzonden. Clients versturen meestal opdrachten naar het domein via een berichtensysteem zoals een wachtrij. De opdrachthandler accepteert deze opdrachten en roept vervolgens methoden van de domein-interface aan. De granulariteit van elke opdracht is zo ontworpen dat de kans op conflicterende aanvragen zo veel mogelijk wordt beperkt. De volgende code toont een overzicht van de klasse 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)
{
...
}
}
Volgende stappen
De volgende patronen en richtlijnen zijn handig bij de implementatie van dit patroon:
- Horizontale, verticale en functionele gegevenspartitionering. Beschrijft aanbevolen procedures voor het verdelen van gegevens in partities die afzonderlijk kunnen worden beheerd en geopend om de schaalbaarheid te verbeteren, conflicten te verminderen en prestaties te optimaliseren.
Verwante resources
Gebeurtenisbronnenpatroon. Hierin wordt beschreven hoe u Gebeurtenisbronnen gebruikt met het CQRS-patroon. Het laat zien hoe u taken in complexe domeinen vereenvoudigt terwijl u de prestaties, schaalbaarheid en reactiesnelheid verbetert. Ook wordt uitgelegd hoe u consistentie kunt bieden voor transactionele gegevens, terwijl volledige audittrails en -geschiedenis worden onderhouden waarmee compenserende acties kunnen worden ingeschakeld.
Gerealiseerde weergave-patroon. Het leesmodel van een CQRS-implementatie kan gerealiseerde weergaven van de gegevens in het schrijfmodel bevatten, of het leesmodel kan worden gebruikt voor het genereren van gerealiseerde weergaven.