CQRS-patroon

Azure Storage

CQRS staat voor Scheiding van opdracht- en queryverantwoordelijkheid, een patroon dat lees- en updatebewerkingen voor een gegevensarchief scheidt. Het implementeren van CQRS in uw toepassing kan de prestaties, schaalbaarheid en beveiliging maximaliseren. Dankzij de flexibiliteit die is gemaakt door te migreren naar CQRS, kan een systeem zich na verloop van tijd beter ontwikkelen en voorkomt u dat bijwerkopdrachten samenvoegingsconflicten veroorzaken op domeinniveau.

Context en probleem

In traditionele architecturen wordt hetzelfde gegevensmodel gebruikt voor het raadplegen en bijwerken van een database. Dat is eenvoudig en werkt prima voor eenvoudige CRUD-bewerkingen. In complexere toepassingen kan deze aanpak echter onpraktisch worden. Zo kan de toepassing aan de leeskant een groot aantal verschillende query's uitvoeren, die DTO's (Data Transfer Objects of objecten voor gegevensoverdracht) met verschillende vormen retourneren. Objecttoewijzing kan ingewikkeld worden. Aan de schrijfkant kan het model complexe validatie en bedrijfslogica implementeren. Hierdoor bestaat de kans dat u uiteindelijk een model hebt dat veel te complex is en dat meer doet dan nodig is.

Lees- en schrijfworkloads zijn vaak asymmetrisch, met zeer verschillende prestatie- en schaalvereisten.

Een traditionele CRUD-architectuur

  • Er is vaak een onjuiste overeenkomst tussen de lees- en schrijfweergaven van de gegevens, zoals extra kolommen of eigenschappen die correct moeten worden bijgewerkt, ook al zijn ze niet vereist als onderdeel van een bewerking.

  • Gegevensconflicten kunnen optreden wanneer bewerkingen parallel worden uitgevoerd op dezelfde set gegevens.

  • De traditionele benadering kan een negatief effect hebben op de prestaties vanwege belasting van het gegevensarchief en de laag voor gegevenstoegang, en de complexiteit van query's die nodig zijn om informatie op te halen.

  • Het beheren van beveiliging en machtigingen kan complex worden, omdat elke entiteit onderhevig is aan zowel lees- als schrijfbewerkingen, waardoor gegevens mogelijk in de verkeerde context worden weergegeven.

Oplossing

CQRS scheidt lees- en schrijfbewerkingen in verschillende modellen, met behulp van opdrachten voor het bijwerken van gegevens en query's om gegevens te lezen.

  • Opdrachten moeten op taken zijn gebaseerd, in plaats van op gegevens gericht. ("Boek hotelkamer", niet "set ReservationStatus to Reserved"). Hiervoor zijn mogelijk enkele bijbehorende wijzigingen in de gebruikersinteractiestijl vereist. Het andere deel hiervan is om te kijken naar het wijzigen van de bedrijfslogica waarmee deze opdrachten vaker worden verwerkt. Een techniek die dit ondersteunt, is het uitvoeren van enkele validatieregels op de client, zelfs voordat de opdracht wordt verzonden, mogelijk knoppen uitschakelen, waarin wordt uitgelegd waarom in de gebruikersinterface (geen ruimten meer). Op die manier kan de oorzaak van opdrachtfouten aan de serverzijde worden beperkt tot racevoorwaarden (twee gebruikers die de laatste ruimte proberen te boeken), en zelfs die kunnen soms worden aangepakt met wat meer gegevens en logica (waarbij een gast op een wachtlijst staat).
  • Opdrachten kunnen in een wachtrij worden geplaatst voor asynchrone verwerking, in plaats van synchroon te worden verwerkt.
  • De uitvoering van query's heeft nooit gevolgen voor de inhoud van de database. Een query retourneert een DTO zonder dat hierbij kennis van het domein wordt ingekapseld.

De modellen kunnen vervolgens worden geïsoleerd, zoals wordt weergegeven in het volgende diagram, hoewel dat geen absolute vereiste is.

Een eenvoudige CQRS-architectuur

Het hebben van afzonderlijke query- en updatemodellen vereenvoudigt het ontwerp en de implementatie. Een nadeel is echter dat CQRS-code niet automatisch kan worden gegenereerd op basis van een databaseschema met behulp van scaffolding-mechanismen zoals O/RM-hulpprogramma's (U kunt echter uw aanpassing bouwen op basis van de gegenereerde code).

Voor een nog grotere isolatie kunt u de leesgegevens fysiek scheiden van de schrijfgegevens. In dat geval kan de leesdatabase een eigen gegevensschema gebruiken dat is geoptimaliseerd voor query's. Zo kan er een gerealiseerde weergave van de gegevens worden opgeslagen, teneinde complexe joins of complexe O/RM-toewijzingen te voorkomen. Er kan zelfs een ander type gegevensarchief worden gebruikt. De schrijfdatabase kan bijvoorbeeld relationeel zijn, terwijl de leesdatabase een documentdatabase is.

Als afzonderlijke lees- en schrijfdatabases worden gebruikt, moeten ze gesynchroniseerd worden gehouden. Dit wordt meestal bereikt door het schrijfmodel een gebeurtenis te laten publiceren wanneer deze de database bijwerken. Zie gebeurtenisgestuurde architectuurstijl voor meer informatie over het gebruik van gebeurtenissen. Aangezien berichtbrokers en -databases meestal niet kunnen worden opgenomen in één gedistribueerde transactie, kunnen er uitdagingen zijn bij het garanderen van consistentie bij het bijwerken van de database en publicatiegebeurtenissen. Zie de richtlijnen voor het verwerken van idempotent berichten voor meer informatie.

Een CQRS-architectuur met afzonderlijke archieven voor lezen en schrijven

Het leesarchief kan een alleen-lezen replica van het schrijfarchief zijn, of de lees- en schrijfarchieven kunnen elk een andere structuur hebben. Het gebruik van meerdere alleen-lezen replica's kan de queryprestaties verbeteren, met name in gedistribueerde scenario's waarin alleen-lezen replica's zich dicht bij de toepassingsexemplaren bevinden.

Het scheiden van de lees- en schrijfarchieven betekent dat elk archief afhankelijk van de belasting afzonderlijk kan worden geschaald. Zo is de belasting van leesarchieven meestal veel hoger belasting dan van schrijfarchieven.

Sommige implementaties van CQRS gebruiken het patroon Gebeurtenisbronnen. Met dit patroon wordt de toepassingsstatus opgeslagen als een reeks gebeurtenissen. Elke gebeurtenis stelt een reeks wijzigingen van de gegevens voor. De huidige status wordt samengesteld door de gebeurtenissen opnieuw af te spelen. In een CQRS-context is het ene voordeel van Event Sourcing dat dezelfde gebeurtenissen kunnen worden gebruikt om andere onderdelen op de hoogte te stellen, met name om het leesmodel op de hoogte te stellen. Het leesmodel maakt gebruik van de gebeurtenissen om een momentopname te maken van de huidige status, een procedure die efficiënter is voor query's. Het patroon Gebeurtenisbronnen voegt echter wel complexiteit toe aan het ontwerp.

Voordelen van CQRS zijn:

  • Onafhankelijk schalen. CQRS maakt het mogelijk om de workload voor lezen en schrijven afzonderlijk van elkaar te schalen, waardoor de kans op vergrendelingsconflicten kleiner wordt.
  • Geoptimaliseerde gegevensschema's. Aan de leeskant kan een schema worden gebruikt dat is geoptimaliseerd voor query's, terwijl aan de schrijfkant een schema kan worden toegepast dat is geoptimaliseerd voor updates.
  • Beveiliging. Het is eenvoudiger om ervoor te zorgen dat alleen de juiste domein-entiteiten schrijfbewerkingen uitvoeren op de gegevens.
  • Scheiding van taken. Het scheiden van de lees- en schrijfbewerkingen kan ertoe bijdragen dat de modellen makkelijker kunnen worden onderhouden en flexibeler zijn. Het merendeel van de complexe bedrijfslogica is nodig voor het schrijfmodel. Het leesmodel kan relatief eenvoudig zijn.
  • Eenvoudigere query's. Door een gerealiseerde weergave op te slaan in de leesdatabase, hoeft de toepassing geen complexe joins te gebruiken tijdens query's.

Implementatieproblemen en overwegingen

Enkele uitdagingen bij het implementeren van dit patroon zijn:

  • Complexiteit. Het uitgangspunt van CQRS is eenvoudig. Maar het patroon kan leiden tot een meer complex toepassingsontwerp, zeker als het patroon Gebeurtenisbronnen wordt toegevoegd.

  • Berichtenuitwisseling. Hoewel berichtenuitwisseling niet vereist is voor CQRS, is deze techniek gebruikelijk voor het verwerken van opdrachten en het publiceren van bijwerkgebeurtenissen. In dat geval moet de toepassing berichtfouten of dubbele berichten afhandelen. Zie de richtlijnen voor Prioriteitswachtrijen voor het verwerken van opdrachten met verschillende prioriteiten.

  • Uiteindelijke consistentie. Als u afzonderlijke databases gebruikt voor lezen en schrijven, zijn de gelezen gegevens mogelijk verouderd. Het leesmodelarchief moet worden bijgewerkt om wijzigingen in het schrijfmodelarchief weer te geven. Het kan lastig zijn om te detecteren wanneer een gebruiker een aanvraag heeft uitgegeven op basis van verouderde leesgegevens.

Wanneer gebruikt u het CQRS-patroon

Overweeg CQRS voor de volgende scenario's:

  • Samenwerkingsdomeinen waarbij veel gebruikers dezelfde gegevens parallel openen. Met CQRS kunt u opdrachten met voldoende granulariteit definiëren om samenvoegingsconflicten op domeinniveau te minimaliseren en conflicten die wel optreden, kunnen door de opdracht worden samengevoegd.

  • Taakgebaseerde gebruikersinterfaces waarin gebruikers via een reeks stappen of met complexe domeinmodellen door een complex proces worden geleid. 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 (een statistische, in DDD-terminologie) en ervoor zorgen dat deze objecten altijd een consistente status hebben. Het leesmodel heeft geen bedrijfslogica of validatiestack en retourneert alleen een DTO voor gebruik in een weergavemodel. Het leesmodel is uiteindelijk consistent met het schrijfmodel.

  • Scenario's waarbij de prestaties van gegevensleesbewerkingen afzonderlijk moeten worden afgestemd op de prestaties van gegevensschrijfbewerkingen, met name wanneer het aantal leesbewerkingen veel groter is dan het aantal schrijfbewerkingen. In dit scenario kunt u het leesmodel uitschalen, maar het schrijfmodel uitvoeren op slechts een paar instanties. Een klein aantal exemplaren van het schrijfmodel helpt ook bij het reduceren van het aantal samenvoegconflicten.

  • Scenario's waarin één team van ontwikkelaars zich kan concentreren op het complexe domeinmodel dat deel uitmaakt van het schrijfmodel, terwijl een ander team zich kan richten op het leesmodel en de gebruikersinterfaces.

  • Scenario's waarin het systeem zich naar verwachting in de loop der tijd zal ontwikkelen en mogelijk meerdere versies van het model bevat of waarin bedrijfsregels regelmatig veranderen.

  • Integratie met andere systemen, met name in combinatie met 'event sourcing', waarbij de tijdelijke uitval van een subsysteem geen gevolgen mag hebben voor de beschikbaarheid van andere systemen.

Dit patroon wordt niet aanbevolen wanneer:

  • Het domein of de bedrijfsregels zijn eenvoudig.

  • Een eenvoudige CRUD-gebruikersinterface en gegevenstoegangsbewerkingen zijn voldoende.

Het is het overwegen waard om CQRS toe te passen op beperkte onderdelen van uw systeem, waar deze aanpak het meest zinvol is.

Workloadontwerp

Een architect moet evalueren hoe het CQRS-patroon kan worden gebruikt in het ontwerp van hun workload om de doelstellingen en principes te verhelpen die worden behandeld in de pijlers van het 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.

Patroon Gebeurtenisbronnen en CQRS

Het CQRS-patroon wordt vaak samen gebruikt met het patroon Gebeurtenisbronnen (Event Sourcing). CQRS-systemen gebruiken afzonderlijke modellen voor het lezen en schrijven van gegevens, die elk zijn afgestemd op relevante taken en die zich vaak in fysiek gescheiden archieven bevinden. Bij gebruik met het patroon Gebeurtenisbronnen is het archief met gebeurtenissen het schrijfmodel en is dit de officiële bron van gegevens. Het leesmodel van een systeem met CQRS biedt gerealiseerde weergaven van de gegevens, meestal als sterk gedenormaliseerde weergaven. Deze weergaven zijn afgestemd op de interfaces en weergavevereisten van de toepassing, waardoor zowel de weergaveprestaties als de queryprestaties worden gemaximaliseerd.

Door de stroom met gebeurtenissen als het schrijfarchief te gebruiken, in plaats van de werkelijke gegevens op een bepaald moment, worden bijwerkconflicten in een combinatie voorkomen en worden de prestaties en schaalbaarheid gemaximaliseerd. De gebeurtenissen kunnen asynchroon worden gebruikt voor het genereren van gerealiseerde weergaven van de gegevens die worden gebruikt voor het vullen van het leesarchief.

Omdat het archief met gebeurtenissen de officiële bron van gegevens is, is het mogelijk om de gerealiseerde weergaven te verwijderen en alle eerdere gebeurtenissen opnieuw af te spelen om een nieuwe weergave van de huidige status te maken als het systeem zich verder ontwikkelt, of wanneer het leesmodel moet worden aangepast. De gerealiseerde weergaven zijn in feite een duurzame alleen-lezen cache van de gegevens.

Houd rekening met het volgende als u CQRS gebruikt in combinatie met het patroon Gebeurtenisbronnen:

  • Net als bij elk systeem waarin de lees- schrijfarchieven van elkaar zijn gescheiden, zijn systemen op basis van dit patroon alleen uiteindelijk consistent. Er is namelijk altijd een vertraging tussen het genereren van de gebeurtenis en het bijwerken van het gegevensarchief.

  • Het patroon voegt complexiteit toe omdat er code moet worden gemaakt voor het initiëren en verwerken van gebeurtenissen, en voor het samenstellen of bijwerken van de juiste weergaven of objecten die worden vereist door query's of een leesmodel. De complexiteit van het CQRS-patroon bij gebruik in combinatie met het patroon Gebeurtenisbronnen kan een geslaagde implementatie bemoeilijken en vereist een andere benadering voor het ontwerpen van systemen. 'Event sourcing' kan het modelleren van het domein echter vereenvoudigen en maakt het gemakkelijker om weergaven opnieuw samen te stellen of om nieuwe weergaven te maken omdat de bedoeling van de wijzigingen in de gegevens behouden blijft.

  • Het genereren van gerealiseerde weergaven voor gebruik in het leesmodel of voor projecties van de gegevens door de gebeurtenissen voor specifieke entiteiten of verzamelingen entiteiten opnieuw af te spelen en te verwerken, kan aanzienlijke verwerkingstijd en resources vereisen. Dit is met name zo als hiervoor gedurende langere perioden optelling of analyse van waarden nodig is, omdat dan de bijbehorende gebeurtenissen mogelijk moeten worden onderzocht. Los dit op door momentopnamen van de gegevens op geplande intervallen te implementeren, zoals een totaalaantal van het aantal specifieke acties dat is opgetreden, of de huidige status van een entiteit.

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:

Blogberichten van Martin Fowler:

  • Gebeurtenisbronnenpatroon. Hier vindt u gedetailleerde informatie over hoe 'event sourcing' kan worden gebruikt met het CQRS-patroon om taken in complexe domeinen te vereenvoudigen en tegelijkertijd de prestaties, schaalbaarheid en respons te verbeteren. Daarnaast wordt hier uitgelegd hoe u consistentie kunt bieden voor transactiegegevens met behoud van volledige audittrails en geschiedenis die compenserende maatregelen mogelijk maken.

  • 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.

  • Presentatie over betere CQRS via asynchrone gebruikersinteractiepatronen