Redigera

Dela via


CQRS-mönster

Azure Storage

CQRS (Command Query Responsibility Segregation) är ett designmönster som separerar läs- och skrivåtgärder för ett datalager i separata datamodeller. Detta gör att varje modell kan optimeras oberoende av varandra och kan förbättra prestanda, skalbarhet och säkerhet för ett program.

Kontext och problem

I traditionella arkitekturer används ofta en enda datamodell för både läs- och skrivåtgärder. Den här metoden är enkel och fungerar bra för grundläggande CRUD-åtgärder (se bild 1).

diagram som visar en traditionell CRUD-arkitektur.
Bild 1. En traditionell CRUD-arkitektur.

I takt med att programmen växer blir det dock allt svårare att optimera läs- och skrivåtgärder på en enda datamodell. Läs- och skrivåtgärder har ofta olika prestanda- och skalningsbehov. En traditionell CRUD-arkitektur tar inte hänsyn till den här asymmetrin. Det leder till flera utmaningar:

  • Datamatchningsfel: Läs- och skrivrepresentationerna av data skiljer sig ofta åt. Vissa fält som krävs under uppdateringar kan vara onödiga under läsningar.

  • Lås konkurrens: Parallella åtgärder på samma datauppsättning kan orsaka låskonkurration.

  • Prestandaproblem: Den traditionella metoden kan ha en negativ effekt på prestanda på grund av belastningen på datalagret och dataåtkomstskiktet samt komplexiteten i frågor som krävs för att hämta information.

  • säkerhetsproblem: Det blir svårt att hantera säkerhet när entiteter omfattas av läs- och skrivåtgärder. Den här överlappningen kan exponera data i oavsiktliga kontexter.

Att kombinera dessa ansvarsområden kan resultera i en alltför komplicerad modell som försöker göra för mycket.

Lösning

Använd CQRS-mönstret för att separera skrivåtgärder (kommandon) från läsåtgärder (frågor). Kommandon ansvarar för att uppdatera data. Frågor ansvarar för att hämta data.

Förstå kommandon. Kommandon bör representera specifika affärsuppgifter i stället för datauppdateringar på låg nivå. I en hotellbokningsapp använder du till exempel "Boka hotellrum" i stället för "Ange ReservationStatus till Reserverad". Den här metoden återspeglar bättre avsikten bakom användaråtgärder och justerar kommandon med affärsprocesser. För att säkerställa att kommandona lyckas kan du behöva förfina användarinteraktionsflödet, logiken på serversidan och överväga asynkron bearbetning.

Förfiningsområde Rekommendation
Validering på klientsidan Verifiera vissa villkor innan du skickar kommandot för att förhindra uppenbara fel. Om det till exempel inte finns några tillgängliga rum inaktiverar du knappen "Boka" och anger ett tydligt, användarvänligt meddelande i användargränssnittet som förklarar varför det inte går att boka. Den här konfigurationen minskar onödiga serverbegäranden och ger omedelbar feedback till användarna, vilket förbättrar deras upplevelse.
Logik på serversidan Förbättra affärslogik för att hantera gränsfall och fel på ett korrekt sätt. Om du till exempel vill ta itu med konkurrensförhållanden (flera användare som försöker boka det senaste tillgängliga rummet) kan du överväga att lägga till användare i en väntelista eller föreslå alternativa alternativ.
Asynkron bearbetning Du kan också bearbeta kommandon asynkront genom att placera dem i en kö i stället för att hantera dem synkront.

Förstå frågor. Frågor ändrar aldrig data. I stället returnerar de dataöverföringsobjekt (DTU:er) som presenterar nödvändiga data i ett bekvämt format, utan någon domänlogik. Denna tydliga uppdelning av problem förenklar systemets utformning och implementering.

Förstå separation av läs- och skrivmodell

Om du separerar läsmodellen från skrivmodellen förenklas systemdesignen och implementeringen genom att olika problem för dataskrivningar och läsningar åtgärdas. Den här separationen förbättrar tydligheten, skalbarheten och prestandan, men medför vissa kompromisser. Exempelvis kan scaffolding-verktyg som O/RM-ramverk inte automatiskt generera CQRS-kod från ett databasschema, vilket kräver anpassad logik för att överbrygga klyftan.

I följande avsnitt utforskas två primära metoder för att implementera modellavgränsning för läsning och skrivning i CQRS. Varje metod har unika fördelar och utmaningar, till exempel synkronisering och konsekvenshantering.

Separation av modeller i ett enda datalager

Den här metoden representerar den grundläggande nivån för CQRS, där både läs- och skrivmodellerna delar en enda underliggande databas men upprätthåller en distinkt logik för sina åtgärder. Genom att definiera separata problem förbättrar den här strategin enkelheten samtidigt som den ger fördelar med skalbarhet och prestanda för vanliga användningsfall. Med en grundläggande CQRS-arkitektur kan du avgränsa skrivmodellen från läsmodellen samtidigt som du förlitar dig på ett delat datalager (se bild 2).

diagram som visar en grundläggande CQRS-arkitektur.
Bild 2. En grundläggande CQRS-arkitektur med ett enda datalager.

Den här metoden förbättrar tydlighet, prestanda och skalbarhet genom att definiera distinkta modeller för hantering av skriv- och läsproblem:

  • Write-modell: Utformad för att hantera kommandon som uppdaterar eller bevarar data. Den innehåller validering, domänlogik och säkerställer datakonsekvens genom att optimera för transaktionsintegritet och affärsprocesser.

  • Läs modell: utformad för att hantera frågor för att hämta data. Den fokuserar på att generera DTU:er (dataöverföringsobjekt) eller projektioner som är optimerade för presentationslagret. Det förbättrar frågeprestanda och svarstider genom att undvika domänlogik.

Fysisk separation av modeller i separata datalager

En mer avancerad CQRS-implementering använder distinkta datalager för läs- och skrivmodellerna. Genom att separera datalager för läsning och skrivning kan du skala var och en så att de matchar belastningen. Du kan också använda en annan lagringsteknik för varje datalager. Du kan använda en dokumentdatabas för läsdatalagret och en relationsdatabas för skrivdatalagret (se bild 3).

diagram som visar en CQRS-arkitektur med separata datalager för läsning och skrivning.
Bild 3. En CQRS-arkitektur med separata läs- och skrivdatalager.

Synkronisera separata datalager: När du använder separata arkiv måste du se till att båda förblir synkroniserade. Ett vanligt mönster är att låta skrivmodellen publicera händelser när den uppdaterar databasen, som läsmodellen använder för att uppdatera sina data. Mer information om hur du använder händelser finns i händelsedriven arkitekturstil. Du kan dock vanligtvis inte registrera meddelandeköer och databaser i en enda distribuerad transaktion. Det kan därför finnas utmaningar med att garantera konsekvens när du uppdaterar databasen och publicerar händelser. Mer information finns i idempotent meddelandebearbetning.

Läs datalager: Läsdatalagret kan använda ett eget dataschema som är optimerat för frågor. Den kan till exempel lagra en materialiserad vy av data för att undvika komplexa kopplingar eller O/RM-mappningar. Läsarkivet kan vara en skrivskyddad replik av skrivarkivet eller ha en annan struktur. Om du distribuerar flera skrivskyddade repliker kan du förbättra prestandan genom att minska svarstiden och öka tillgängligheten, särskilt i distribuerade scenarier.

Fördelar med CQRS

  • Oberoende skalning. Med CQRS kan läs- och skrivmodellerna skalas separat, vilket kan minimera låskonkurrensen och förbättra systemets prestanda under belastning.

  • Optimerade datascheman. Läsåtgärder kan använda ett schema som är optimerat för frågor. Skrivåtgärder använder ett schema som är optimerat för uppdateringar.

  • Säkerhet. Genom att separera läsningar och skrivningar kan du se till att endast lämpliga domänentiteter eller åtgärder har behörighet att utföra skrivåtgärder på data.

  • Separering av problem. Att dela upp läs- och skrivansvaret resulterar i renare och mer underhållsbara modeller. Skrivsidan hanterar vanligtvis komplex affärslogik, medan lässidan kan förbli enkel och fokusera på frågeeffektivitet.

  • Enklare frågor. När du lagrar en materialiserad vy i läsdatabasen kan programmet undvika komplexa kopplingar vid frågor.

Implementeringsproblem och överväganden

Några utmaningar med att implementera det här mönstret är:

  • Ökad komplexitet. Även om kärnkonceptet för CQRS är enkelt kan det medföra betydande komplexitet i programdesignen, särskilt när det kombineras med mönstret Händelsekällor.

  • Messaging-utmaningar. Även om meddelanden inte är ett krav för CQRS använder du det ofta för att bearbeta kommandon och publicera uppdateringshändelser. När meddelanden är inblandade måste systemet ta hänsyn till potentiella problem, till exempel meddelandefel, dubbletter och återförsök. Se vägledningen om Prioritetsköer för strategier för att hantera kommandon med olika prioriteringar.

  • Eventuell konsekvens. När läs- och skrivdatabaserna separeras kanske inte läsdata återspeglar de senaste ändringarna omedelbart, vilket leder till inaktuella data. Det kan vara svårt att se till att läsmodellarkivet förblir up-to-date med ändringar i skrivmodellarkivet. Dessutom kräver det noggrant övervägande att identifiera och hantera scenarier där en användare agerar på inaktuella data.

När du ska använda CQRS-mönster

CQRS-mönstret är användbart i scenarier som kräver en tydlig uppdelning mellan dataändringar (kommandon) och datafrågor (läsningar). Överväg att använda CQRS i följande situationer:

  • Samarbetsdomäner: I miljöer där flera användare får åtkomst till och ändrar samma data samtidigt hjälper CQRS till att minska sammanslagningskonflikter. Kommandon kan innehålla tillräckligt med kornighet för att förhindra konflikter, och systemet kan lösa alla som uppstår inom kommandologiken.

  • Aktivitetsbaserade användargränssnitt: Program som vägleder användare genom komplexa processer som en serie steg eller med komplexa domänmodeller drar nytta av CQRS.

    • Skrivmodellen har en fullständig kommandobearbetningsstack med affärslogik, indataverifiering och affärsverifiering. Skrivmodellen kan behandla en uppsättning associerade objekt som en enda enhet för dataändringar, som kallas aggregering i domändriven designterminologi. Skrivmodellen kan också se till att dessa objekt alltid är i ett konsekvent tillstånd.

    • Läsmodellen har ingen affärslogik eller valideringsstack. Den returnerar en DTO för användning i en vymodell. Läsningsmodellen är i slutlig överensstämmelse med skrivningsmodellen.

  • Prestandajustering: System där prestanda för dataläsningar måste finjusteras separat från prestanda för dataskrivningar, särskilt när antalet läsningar är större än antalet skrivningar, drar nytta av CQRS. Läsmodellen skalas horisontellt för att hantera stora frågevolymer, medan skrivmodellen körs på färre instanser för att minimera sammanslagningskonflikter och upprätthålla konsekvens.

  • Separation av utvecklingsproblem: CQRS gör det möjligt för team att arbeta oberoende av varandra. Ett team fokuserar på att implementera den komplexa affärslogik i skrivmodellen, medan en annan utvecklar läsmodellen och komponenterna i användargränssnittet.

  • Utvecklande system: CQRS stöder system som utvecklas över tid. Den rymmer nya modellversioner, frekventa ändringar av affärsregler eller andra ändringar utan att påverka befintliga funktioner.

  • Systemintegrering: System som integreras med andra undersystem, särskilt de som använder händelsekällor, förblir tillgängliga även om ett undersystem tillfälligt misslyckas. CQRS isolerar fel, vilket förhindrar att en enskild komponent påverkar hela systemet.

När du inte ska använda CQRS

Undvik CQRS i följande situationer:

  • Domänen eller affärsreglerna är enkla.

  • Det räcker med ett enkelt CRUD-gränssnitt och dataåtkomståtgärder.

Design av arbetsbelastning

En arkitekt bör utvärdera hur CQRS-mönstret används i arbetsbelastningens design för att uppfylla de mål och principer som beskrivs i Azure Well-Architected Framework-pelare. Till exempel:

Grundpelare Så här stöder det här mönstret pelarmål
Prestandaeffektivitet hjälper din arbetsbelastning att effektivt uppfylla kraven genom optimeringar inom skalning, data och kod. Separationen av läs- och skrivåtgärder i hög läs-till-skriv-arbetsbelastningar möjliggör riktade prestanda- och skaloptimeringar för varje åtgärds specifika syfte.

- PE:05 Skalning och partitionering
- PE:08 Dataprestanda

Som med alla designbeslut bör du överväga eventuella kompromisser mot målen för de andra pelarna som kan införas med det här mönstret.

Kombinera händelsekällor och CQRS

Vissa implementeringar av CQRS innehåller mönstret händelsekällor, som lagrar systemets tillstånd som en kronologisk serie händelser. Varje händelse registrerar de ändringar som gjorts i data vid en viss tidpunkt. För att fastställa det aktuella tillståndet spelar systemet upp dessa händelser i ordning. I den här kombinationen:

  • Händelsearkivet är den skrivmodellen och den enda sanningskällan.

  • Den läsmodellen genererar materialiserade vyer från dessa händelser, vanligtvis i ett mycket avnormaliserat format. Dessa vyer optimerar datahämtningen genom att skräddarsy strukturer för att fråga efter och visa krav.

Fördelar med att kombinera händelsekällor och CQRS

Samma händelser som uppdaterar skrivmodellen kan fungera som indata till läsmodellen. Läsmodellen kan sedan skapa en ögonblicksbild i realtid av det aktuella tillståndet. Dessa ögonblicksbilder optimerar frågor genom att tillhandahålla effektiva, förberäknade vyer av data.

I stället för att lagra det aktuella tillståndet direkt använder systemet en ström av händelser som skrivarkiv. Den här metoden minskar uppdateringskonflikter i aggregeringar och förbättrar prestanda och skalbarhet. Systemet kan bearbeta dessa händelser asynkront för att skapa eller uppdatera materialiserade vyer för läsarkivet.

Eftersom händelsearkivet fungerar som en enda sanningskälla kan du enkelt återskapa materialiserade vyer eller anpassa dig till ändringar i läsmodellen genom att spela upp historiska händelser igen. I grund och botten fungerar materialiserade vyer som en beständig, skrivskyddad cache som är optimerad för snabba och effektiva frågor.

Överväganden vid kombination av händelsekällor och CQRS

Innan du kombinerar CQRS-mönstret med mönstret Händelsekällor bör du utvärdera följande överväganden:

  • Slutlig konsekvens: Eftersom skriv- och läsarkiven är separata kan uppdateringar av läsarkivet ligga efter händelsegenereringen, vilket resulterar i slutlig konsekvens.

  • Ökad komplexitet: att kombinera CQRS med händelsekällor kräver en annan designmetod, vilket kan göra en lyckad implementering mer utmanande. Du måste skriva kod för att generera, bearbeta och hantera händelser och sammanställa eller uppdatera vyer för läsmodellen. Händelsekällor förenklar dock domänmodellering och gör att du enkelt kan återskapa eller skapa nya vyer genom att bevara historiken och avsikten för alla dataändringar.

  • Prestanda för visningsgenerering: Generera materialiserade vyer för läsmodellen kan förbruka betydande tid och resurser. Detsamma gäller för projektering av data genom att spela upp och bearbeta händelser för specifika entiteter eller samlingar. Den här effekten ökar när beräkningar omfattar analys eller summering av värden under långa perioder, eftersom alla relaterade händelser måste undersökas. Implementera ögonblicksbilder av data med jämna mellanrum. Lagra till exempel periodiska ögonblicksbilder av aggregerade summor (antalet gånger en specifik åtgärd inträffar) eller det aktuella tillståndet för en entitet. Ögonblicksbilder minskar behovet av att bearbeta hela händelsehistoriken upprepade gånger, vilket förbättrar prestandan.

Exempel på CQRS-mönster

Följande kod visar vissa utdrag ur ett exempel på en CQRS-implementation som använder olika definitioner för läsnings- och skrivningsmodeller. Modellgränssnitten kräver inte några speciella funktioner i underliggande datalagringsplatser, och de kan utvecklas och finjusteras oberoende eftersom dessa gränssnitt är åtskilda.

Följande kod visar läsningsmodellens definition.

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

Systemet tillåter användare att betygsätta produkter. Programkoden gör detta med hjälp av kommandot RateProduct som visas i följande kod.

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

Systemet använder klassen ProductsCommandHandler för att hantera kommandon som skickas av programmet. Klienter skickar vanligtvis kommandon till domänen via ett meddelandesystem, till exempel en kö. Kommandohanteraren accepterar dessa kommandon och anropar metoder i domängränssnittet. Granulariteten för varje kommando har utformats för att minska risken för begäranden som står i konflikt. Följande kod visar en översikt över klassen 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)
  {
    ...
  }
}

Nästa steg

Följande mönster och riktlinjer kan vara relevanta när du implementerar det här mönstret:

  • Mönstret Händelsekällor. Beskriver hur du använder händelsekällor med CQRS-mönstret. Den visar hur du förenklar uppgifter i komplexa domäner samtidigt som du förbättrar prestanda, skalbarhet och svarstider. Den förklarar också hur du ger konsekvens för transaktionsdata samtidigt som fullständiga spårningsloggar och historik bibehålls som kan aktivera kompenserande åtgärder.

  • Mönster för materialiserad vy. Läsningsmodellen i en CQRS-implementering kan innehålla materialiserade vyer av skrivningsmodelldata, eller läsningsmodellen kan användas för att skapa materialiserade vyer.