CQRS-minta

Azure Storage

A CQRS a parancs- és lekérdezési felelősség elkülönítését jelenti, amely az adattárak olvasási és frissítési műveleteit választja el egymástól. A CQRS alkalmazása maximalizálhatja a teljesítményt, a méretezhetőséget és a biztonságot. A CQRS-re való migrálás által létrehozott rugalmasság lehetővé teszi, hogy a rendszer idővel jobban fejlődjön, és megakadályozza, hogy a frissítési parancsok egyesítési ütközéseket okozzanak a tartomány szintjén.

Kontextus és probléma

A hagyományos architektúrák esetében ugyanaz az adatmodell használatos az adatbázisok lekérdezésére és frissítésére. Ez egyszerű és jól működik, ha alapszintű CRUD-műveletekről van szó. Összetettebb alkalmazások esetében azonban ez a megközelítés nehézkessé válhat. Például az olvasási oldalon az alkalmazás számos különböző lekérdezést végezhet, amelyek különböző típusú adatátviteli objektumokat (DTO-kat) adnak vissza. Az objektumok leképezése igen bonyolulttá válthat. A írási oldalon a modell összetett érvényesítési és üzleti logikát valósíthat meg. Ennek eredményeképpen egy túlzottan összetett, túl sok feladatot végző modell jöhet létre.

Az olvasási és írási számítási feladatok gyakran aszimmetrikusak, nagyon eltérő teljesítménnyel és méretezési követelményekkel.

Hagyományos CRUD-architektúra

  • Az adatok olvasási és írási ábrázolásai között gyakran van eltérés, például további oszlopok vagy tulajdonságok, amelyeket helyesen kell frissíteni, még akkor is, ha a művelet részeként nem szükséges.

  • Az adat versengés akkor fordulhat elő, ha a műveleteket párhuzamosan hajtják végre ugyanazon az adatkészleten.

  • A hagyományos megközelítés negatív hatással lehet a teljesítményre az adattár és az adatelérési réteg terhelése, valamint az információk lekéréséhez szükséges lekérdezések összetettsége miatt.

  • A biztonság és az engedélyek kezelése összetettsé válhat, mivel minden entitásra olvasási és írási műveletek is vonatkoznak, amelyek rossz környezetben tehetnek közzé adatokat.

Megoldás

A CQRS az olvasásokat és az írásokat különböző modellekre választja el, az adatok frissítésére vonatkozó parancsokkal , valamint az adatok olvasására vonatkozó lekérdezésekkel .

  • A parancsok nem adatközpontúak, hanem feladatalapúak. ("Foglalási szoba", nem "reservationStatus beállítása fenntartottra"). Ehhez szükség lehet a felhasználói interakció stílusának néhány megfelelő módosítására. A másik része, hogy vizsgálja meg a módosítás az üzleti logika feldolgozása ezeket a parancsokat, hogy sikeresebb legyen gyakrabban. Az egyik módszer, amely támogatja ezt, hogy futtasson néhány érvényesítési szabályt az ügyfélen még a parancs elküldése előtt, esetleg letiltsa a gombokat, és elmagyarázza, hogy miért a felhasználói felületen ("nem maradt szoba"). Ily módon a kiszolgálóoldali parancshibák oka a versenyfeltételekre szűkíthető (két felhasználó megpróbálja lefoglalni az utolsó szobát), és még azokat is meg lehet oldani néhány további adattal és logikával (egy vendég várólistára helyezése).
  • A parancsok aszinkron feldolgozás céljából várólistára helyezhetők ahelyett, hogy szinkron módon dolgozzák fel őket.
  • A lekérdezések soha nem módosítják az adatbázist. A lekérdezés egy olyan DTO-t ad vissza, amely nem tartalmaz a környezettel kapcsolatos ismerteket.

A modellek ezután elkülöníthetők az alábbi ábrán látható módon, bár ez nem abszolút követelmény.

Alapszintű CQRS-architektúra

A különálló lekérdezési és frissítési modellek megkönnyítik a tervezést és a megvalósítást. Az egyik hátránya azonban az, hogy a CQRS-kód nem hozható létre automatikusan egy adatbázissémából olyan állványzati mechanizmusokkal, mint az O/RM-eszközök (azonban a létrehozott kódra építheti a testreszabást).

A nagyobb mértékű elkülönítés érdekében fizikailag is elkülönítheti az olvasási és az írási adatokat. Ebben az esetben az olvasási adatbázis használhatja a saját, lekérdezésekhez optimalizált adatsémáját. Például tárolhatja az adatok materializált nézetét, a bonyolult illesztések vagy O/RM-leképezések elkerülése érdekében. Sőt, akár eltérő típusú adattárat is használhat. Például az írási adatbázis lehet relációs, míg az olvasási adatbázis lehet egy dokumentum-adatbázis.

Ha külön olvasási és írási adatbázisokat használ, szinkronban kell tartani őket. Ez általában úgy történik, hogy az írási modell közzétesz egy eseményt, amikor frissíti az adatbázist. Az események használatáról további információt az eseményvezérelt architektúrastílusban talál. Mivel az üzenetközvetítők és -adatbázisok általában nem vehetők fel egyetlen elosztott tranzakcióba, kihívást jelenthet a konzisztencia biztosítása az adatbázis frissítésekor és az események közzétételekor. További információkért tekintse meg az idempotens üzenetfeldolgozással kapcsolatos útmutatást.

CQRS-architektúra külön olvasási és írási tárolókkal

Az olvasási tároló lehet az írási tároló csak olvasható replikája, de az olvasási és írási tárolók lehetnek teljesen eltérő szerkezetűek is. Több írásvédett replika használata növelheti a lekérdezési teljesítményt, különösen olyan elosztott helyzetekben, ahol az írásvédett replikák az alkalmazáspéldányok közelében találhatók.

Az olvasási és írási tárolók különválasztása lehetővé teszi a terhelésnek megfelelő méretezésüket is. Az olvasási tárolók például jellemzően jóval nagyobb terhelésnek vannak kitéve, mint az írási tárolók.

Egyes CQRS-megvalósítások az Event Sourcing mintát használják. Ebben a mintában az alkalmazásállapot események sorozataként tárolódik. Az egyes események az adatok módosításainak egy halmazát jelölik. A jelenlegi állapot az események visszajátszása alapján áll össze. A CQRS-környezetben az Event Sourcing egyik előnye, hogy ugyanezek az események más összetevők értesítésére is használhatók – különösen az olvasási modell értesítésére. Az olvasási modell az események használatával pillanatképet készít az aktuális állapotról, ami a lekérdezések esetében hatékonyabb megoldásnak bizonyul. Az Event Sourcing azonban bonyolultabbá teszi a kialakítást.

A CQRS előnyei a következők:

  • Független skálázás. A CQRS lehetővé teszi az olvasási és írási számítási feladatok egymástól független méretezését, ezáltal kevesebb zárolási versenyt eredményezhet.
  • Optimalizált adatsémák. Az olvasási oldal lekérdezésekre optimalizált sémát, az írási oldal pedig frissítésekhez optimalizált sémát használhat.
  • Biztonság. Egyszerűbb meggyőződni arról, hogy csak a megfelelő tartományi entitások végeznek írást az adatokon.
  • Kockázatok elkülönítése. Az olvasási és írási oldalak elkülönítésével könnyebben fenntartható és rugalmas modellek hozhatók létre. A bonyolult üzleti logika legnagyobb része az írási modellbe kerül. Az olvasási modell lehet viszonylag egyszerű.
  • Egyszerűbb lekérdezések. A materializált nézet olvasási adatbázisban való tárolásával elkerülhető, hogy az alkalmazásnak bonyolult illesztésekre legyen szüksége a lekérdezések során.

Megvalósítással kapcsolatos problémák és szempontok

A minta megvalósításának néhány kihívása a következők:

  • Összetettség. A CQRS alapvető működése egyszerű. Viszont bonyolultabbá teheti az alkalmazások kialakítását, különösen akkor, ha az Event Sourcing mintát is tartalmazza.

  • Üzenetkezelés. Bár a CQRS használatához nincs szükség üzenetkezelésre, az üzenetkezelési szolgáltatást gyakorta használják a parancsok feldolgozására és a frissítési események közzétételére. Ebben az esetben az alkalmazásnak kezelnie kell az üzenethibákat és az ismétlődő üzeneteket. A különböző prioritásokkal rendelkező parancsok kezeléséhez tekintse meg a prioritási üzenetsorokkal kapcsolatos útmutatást.

  • Végleges konzisztencia. Ha elkülöníti az olvasási és írási adatbázisokat, az olvasási adatok elavulttá válhatnak. Az olvasási modell tárolót frissíteni kell, hogy tükrözze az írási modell tárolójának változásait, és nehéz lehet észlelni, hogy a felhasználó elavult olvasási adatok alapján adott-e ki kérést.

Mikor érdemes CQRS-mintát használni?

Fontolja meg a CQRS-t a következő forgatókönyvekhez:

  • Együttműködésen alapuló tartományok, ahol sok felhasználó éri el párhuzamosan ugyanazokat az adatokat. A CQRS lehetővé teszi olyan parancsok definiálását, amelyek elegendő részletességgel rendelkeznek az egyesítési ütközések tartományszintű minimalizálásához, és a felmerülő ütközések egyesíthetők a paranccsal.

  • Feladatalapú felhasználói felületek, ahol a felhasználók lépésenként haladnak végig egy összetett folyamaton, vagy összetett tartományi modellekkel. Az írási modell teljes körű parancsfeldolgozási vermet tartalmaz üzleti logikával, bemenet-ellenőrzéssel és üzleti ellenőrzéssel. Az írási modell egyetlen egységként kezelheti a társított objektumokat az adatváltozásokhoz (a DDD terminológiájában összesítve), és gondoskodhat arról, hogy ezek az objektumok mindig konzisztens állapotban legyenek. Az olvasási modell nem tartalmaz üzleti logikát vagy érvényesítési vermet, és csak egy DTO-t ad vissza a nézetmodellben való használathoz. Az olvasási modell véglegesen konzisztens az írási modellel.

  • Azokban az esetekben, amikor az adatolvasások teljesítményét az adatírás teljesítményétől elkülönítve kell finomhangolni, különösen akkor, ha az olvasások száma sokkal nagyobb, mint az írások száma. Ebben a forgatókönyvben felskálázhatja az olvasási modellt, de csak néhány példányon futtathatja az írási modellt. Alacsony számú írási modell-példány használatával csökkenthető az egyesítési ütközések előfordulása is.

  • Olyan forgatókönyvek, amelyekben egy fejlesztői csapat az írási modell részét képező összetett tartományi modellre összpontosíthat, egy másik csapat pedig az olvasási modellre és a felhasználói felületekre.

  • Olyan forgatókönyvek, amelyekben a rendszer az idő előrehaladtával várhatóan fejlődni fog, így a modell több verzióját is tartalmazhatja, illetve ahol az üzleti szabályok rendszeresen változnak.

  • Integráció más rendszerekkel, különösen az Event Sourcinggal kombinálva, amely esetben egy alrendszer átmeneti meghibásodása nem lehet hatással a többi alrendszer rendelkezésre állására.

Ez a minta nem ajánlott a következő esetekben:

  • A tartomány vagy az üzleti szabályok egyszerűek.

  • Elegendő egy egyszerű CRUD-stílusú felhasználói felület és adatelérési művelet.

A CQRS-t érdemes lehet a rendszernek csak az olyan korlátozott részein használni, ahol a leginkább hasznos.

Számítási feladatok tervezése

Az tervezőknek értékelniük kell, hogyan használható a CQRS-minta a számítási feladat kialakításában az Azure Well-Architected Framework pilléreiben foglalt célok és alapelvek kezelésére. Példa:

Pillér Hogyan támogatja ez a minta a pillércélokat?
A teljesítményhatékonyság a skálázás, az adatok és a kód optimalizálásával segíti a számítási feladatok hatékony kielégítését . Az olvasási és írási műveletek elkülönítése magas olvasási-írási számítási feladatokban célzott teljesítményt és skálázási optimalizálást tesz lehetővé az egyes műveletek konkrét céljához.

- PE:05 Skálázás és particionálás
- PE:08 Adatteljesítmény

Mint minden tervezési döntésnél, fontolja meg az ezzel a mintával bevezethető többi pillér céljaival szembeni kompromisszumokat.

Event Sourcing és CQRS minta

A CQRS mintát gyakran használják az Event Sourcing mintával együtt. A CQRS-alapú rendszerek külön olvasási és írási adatmodelleket használnak, amelyek mindegyike a kapcsolódó feladatokhoz van igazítva, és gyakran fizikailag elkülönített tárolóban találhatók. Az Event Sourcing minta használata esetén az események tárolója és az információk hivatalos forrása az írási modell. A CQRS-alapú rendszerek olvasási modellje az adatok materializált nézetit biztosítja, általában nagy mértékben denormalizált nézetekként. Ezek a nézetek az alkalmazás felhasználói felületeihez és megjelenítési követelményeihez vannak igazítva, ezzel pedig maximalizálható a megjelenítési és lekérdezési teljesítmény.

Ha egy adott időpont aktuális adatai helyett az eseménystreamet használja írási tárolóként, azzal elkerülhetők az egyetlen összesítésen felmerülő frissítési ütközések, valamint maximalizálható a teljesítmény és a méretezhetőség. Az események az olvasási tároló feltöltéséhez használt adatok materializált nézetének aszinkron létrehozásához használhatók.

Mivel az eseménytároló az információk hivatalos forrása, a materializált nézetek törölhetők, és a rendszer fejlődésekor az összes múltbeli esemény visszajátszható a jelenlegi állapot új ábrázolásának létrehozása érdekében, illetve ha az olvasási modell módosítására van szükség. A materializált nézetek lényegében az adatok tartós, csak olvasható gyorsítótáraként működnek.

A CQRS és az Event Sourcing minta együttes használatakor vegye figyelembe az alábbiakat:

  • Mint minden olyan rendszernél, ahol az írási és az olvasási adattárak elkülönülnek, az ezen mintán alapuló rendszerek esetében csak a végleges konzisztencia valósul meg. Az esemény létrehozása és az adattár frissítése között bizonyos fokú késésre lehet számítani.

  • A minta összetetté teszi a rendszert, mivel kód létrehozására van szükség az események indításához és kezeléséhez, valamint a lekérdezésekhez vagy az olvasási modellhez szükséges megfelelő nézetek vagy objektumok összeállításához vagy frissítéséhez. A CQRS minta az Event Sourcing mintával való együttes használatából eredő összetettség megnehezítheti a sikeres megvalósítást, és a rendszerek tervezése szempontjából is eltérő megközelítést igényel. Az Event Sourcing azonban megkönnyítheti a tartomány modellezését, illetve a nézetek újraépítését vagy új nézetek létrehozását, mivel az adatok módosításainak célját megőrzi.

  • A materializált nézetek létrehozásához jelentős feldolgozási időre és erőforrás-használatra van szükség, ha azokat az adott entitások vagy entitásgyűjtemények eseményeinek visszajátszásával és kezelésével az olvasási modellben vagy az adatok leképezéseiben használja. Ez különösen akkor igaz, ha hosszú időszakok értékeinek összesítésére vagy elemzésére van szükség, mivel előfordulhat, hogy minden kapcsolódó eseményt meg kell vizsgálni. Ezt úgy oldhatja meg, hogy ütemezett időközönként implementálja az adatok pillanatképeit, például egy adott művelet teljes számát vagy egy entitás aktuális állapotát.

Példa A CQRS-mintára

Az alábbi kód részleteket mutat be egy CQRS-megvalósítási példából, amely külön definíciót használ az olvasási és írási modellekhez. A modellfelületek nem határozzák meg az alapul szolgáló adattárak jellemzőit, továbbá a felületek elkülönülése miatt a fejlődésük és finomhangolásuk is függetlenül valósulhat meg.

A következő kód bemutatja az olvasási modell definícióját.

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

A rendszer lehetővé teszi a felhasználóknak számára a termékek értékelését. Az alkalmazás kódja ezt az alábbi kódban szereplő RateProduct parancs használatával éri el.

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

A rendszer a ProductsCommandHandler osztályt használja az alkalmazás által küldött parancsok kezelésére. Az ügyfelek általában egy üzenetkezelő rendszeren, például egy üzenetsoron keresztül küldenek a parancsokat a tartománynak. A parancskezelő fogadja ezeket a parancsokat, és meghívja a tartományi felület metódusait. Az egyes parancsok részletességének célja a kérések ütközési esélyeinek csökkentése. A következő kód a ProductsCommandHandler osztály vázlatát mutatja be.

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)
  {
    ...
  }
}

Következő lépések

Az alábbi minták és útmutatások hasznosak lehetnek a minta használatakor:

Martin Fowler blogbejegyzései:

  • Event Sourcing minta. Részletesebben ismerteti, hogyan használható az Event Sourcing a CQRS mintával a feladatok egyszerűsítéséhez az összetett tartományokban a teljesítmény, a méretezhetőség és a válaszkészség növelése mellett. Emellett azt is bemutatja, hogyan biztosítható a tranzakciós adatok konzisztenciája a teljes körű naplók és előzmények fenntartása mellett, ami lehetővé teszi a kompenzáló műveletek végrehajtását.

  • Tényleges táblán alapuló nézet minta. A CQRS megvalósítás olvasási modellje tartalmazhatja az írási modell adatainak materializált nézeteit, illetve a materializált nézetek létrehozására is használható.

  • Bemutató a jobb CQRS-ről aszinkron felhasználói interakciós mintákon keresztül