Model CQRS

Azure Storage

CQRS je zkratka pro oddělení odpovědnosti příkazů a dotazů, který odděluje operace čtení a aktualizace úložiště dat. Implementace CQRS ve vaší aplikaci může maximalizovat její výkon, škálovatelnost a zabezpečení. Flexibilita vytvořená migrací na CQRS umožňuje systému lépe vyvíjet v průběhu času a brání v tom, aby příkazy aktualizací způsobovaly konflikty při slučování na úrovni domény.

Kontext a problém

V tradičních architekturách se pro dotazování a aktualizace databáze používá stejný datový model. Je to jednoduché a funguje to dobře pro základní operace CRUD. Ve složitějších aplikacích ale tento přístup může být nepraktický. Na straně čtení může například aplikace provádět mnoho různých dotazů, které vracejí objekty pro přenos dat (DTO) různých tvarů. Mapování objektu se může stát složité. Na straně zápisu může model implementovat komplexní ověřování a obchodní logiku. V důsledku toho můžete skončit s velmi složitým modelem, který umí příliš mnoho.

Úlohy čtení a zápisu jsou často asymetrické, s velmi odlišnými požadavky na výkon a škálování.

Tradiční architektura CRUD

  • Mezi reprezentací dat pro čtení a zápis často dochází k neshodě, například mezi dalšími sloupci nebo vlastnostmi, které se musí správně aktualizovat, i když se v rámci operace nevyžadují.

  • Kolize dat může nastat, když se operace provádějí paralelně se stejnou sadou dat.

  • Tradiční přístup může mít negativní vliv na výkon kvůli zatížení vrstvy úložiště dat a přístupu k datům a složitosti dotazů potřebných k načtení informací.

  • Správa zabezpečení a oprávnění může být složitá, protože každá entita podléhá operacím čtení i zápisu, což může vystavit data v nesprávném kontextu.

Řešení

CQRS odděluje čtení a zápisy do různých modelů pomocí příkazů k aktualizaci dat a dotazů na čtení dat.

  • Příkazy by měly být založené na úlohách, nikoli na data zaměřené. ("Rezervovat hotelový pokoj", nikoli "nastavit ReservationStatus na rezervovaný"). To může vyžadovat některé odpovídající změny stylu interakce uživatele. Druhá část, která spočívá v tom, že se podíváme na úpravu zpracování obchodní logiky, aby tyto příkazy byly častěji úspěšné. Jednou z technik, která to podporuje, je spuštění některých ověřovacích pravidel na klientovi i před odesláním příkazu, případně zakázáním tlačítek, což vysvětluje, proč v uživatelském rozhraní ("žádné místnosti nezůstaly"). Tímto způsobem může být příčina selhání příkazů na straně serveru zúžena na konflikty časování (dva uživatelé se snaží rezervovat poslední místnost) a dokonce i ty, které mohou být někdy vyřešeny s některými dalšími daty a logikou (umístění hosta do seznamu čekání).
  • Příkazy mohou být umístěny do fronty pro asynchronní zpracování, nikoli synchronně.
  • Dotazy nikdy nemění databázi. Dotaz vrací objekt DTO, který neobsahuje žádnou znalost domény.

Modely je pak možné izolovat, jak je znázorněno v následujícím diagramu, i když to není absolutní požadavek.

Základní architektura modelu CQRS

Samostatné modely dotazů a aktualizací zjednodušují návrh a implementaci. Nevýhodou je ale to, že kód CQRS se nedá automaticky vygenerovat ze schématu databáze pomocí mechanismů generování uživatelského rozhraní, jako jsou nástroje pro generování objektů (V/RM (ale budete moct vytvořit vlastní nastavení nad vygenerovaným kódem).

Kvůli větší izolaci můžete fyzicky oddělit čtení dat od zápisu dat. V takovém případě čtení databáze může používat vlastní schéma dat, které je optimalizované pro dotazy. Například může ukládat materializovaná zobrazení dat, aby se zabránilo komplexním spojením nebo komplexním mapováním O/RM. Může dokonce používat jiný typ úložiště dat. Databáze pro zápis může být například relační, zatímco databáze pro čtení je databází dokumentů.

Pokud se používají samostatné databáze pro čtení a zápis, musí být synchronizované. Obvykle toho dosáhnete tak, že model zápisu publikuje událost při každé aktualizaci databáze. Další informace o používání událostí naleznete ve stylu architektury řízené událostmi. Vzhledem k tomu, že zprostředkovatelé zpráv a databáze obvykle nemohou být zařazeny do jedné distribuované transakce, mohou být problémy při zajištění konzistence při aktualizaci databáze a publikování událostí. Další informace najdete v doprovodných materiálech k idempotentnímu zpracování zpráv.

Architektura modelu CQRS se samostatnými úložišti pro čtení a zápis

Úložiště pro čtení může být replikou úložiště pro zápis, která je jen pro čtení, nebo úložiště pro čtení a zápis můžou mít úplně odlišnou strukturu. Použití více replik jen pro čtení může zvýšit výkon dotazů, zejména v distribuovaných scénářích, kdy se repliky jen pro čtení nacházejí blízko instancí aplikace.

Oddělení úložišť pro čtení a zápis také umožňuje škálování každého úložiště zvlášť podle příslušného zatížení. Například u úložišť pro čtení se obvykle vyskytuje mnohem větší zatížení než u úložišť pro zápis.

Některé implementace CQRS používají model Event Sourcing. V tomto modelu se stav aplikace ukládá jako posloupnost událostí. Každá událost představuje sadu změn dat. Aktuální stav se vytvoří přehráním událostí. V kontextu CQRS je jednou z výhod služby Event Sourcing to, že stejné události lze použít k oznamování jiných komponent – zejména k oznamování modelu čtení. Model pro čtení používá události k vytvoření snímku aktuálního stavu, což je efektivnější pro dotazy. Ale model Event Sourcing přidává do návrhu složitost.

Mezi výhody CQRS patří:

  • Nezávislé škálování CQRS umožňuje úlohy čtení a zápisu nezávisle škálovat a může vést k nižšímu počtu kolizí zámků.
  • Optimalizovaná schémata dat. Strana pro čtení může používat schéma, které je optimalizované pro dotazy, zatímco strana pro zápis používá schéma, které je optimalizované pro aktualizace.
  • Zabezpečení. Je jednodušší zajistit, aby zápis dat prováděly jenom entity správné domény.
  • Oddělení oblastí zájmu. Oddělení stran pro čtení a zápis může vést k modelům, které jsou snadněji udržovatelné a flexibilní. Většina komplexní obchodní logiky spadá do modelu zápisu. Model čtení může být poměrně jednoduchý.
  • Jednodušší dotazy. Uložením materializovaného zobrazení v databázi pro čtení umožňuje aplikaci vyhnout se komplexním spojením při dotazování.

Problémy a aspekty implementace

Mezi problémy při implementaci tohoto modelu patří:

  • Složitost: Základní myšlenka CQRS je jednoduchá. Ale může vést ke složitějšímu návrhu aplikace, zejména v případě, že obsahuje model Event Sourcing.

  • Zasílání zpráv. I když CQRS nevyžaduje zasílání zpráv, běžně se používá ke zpracování příkazů a publikování událostí aktualizace. V takovém případě musí aplikace umět zpracovat selhání zpráv nebo duplicitní zprávy. Pokyny k prioritním frontám pro práci s příkazy s různými prioritami

  • Konečná konzistence. Pokud oddělíte databáze pro čtení a zápis, mohou být čtená data zastaralá. Úložiště modelu čtení musí být aktualizováno tak, aby odráželo změny v úložišti modelů zápisu a může být obtížné zjistit, kdy uživatel vydal požadavek na základě zastaralých dat čtení.

Kdy použít model CQRS

Pro následující scénáře zvažte CQRS:

  • Domény pro spolupráci, kde mnoho uživatelů přistupuje ke stejným datům paralelně CQRS umožňuje definovat příkazy s dostatečnou členitostí pro minimalizaci konfliktů při slučování na úrovni domény a konflikty, ke kterým dojde, může příkaz sloučit.

  • Textová uživatelská rozhraní, která uživatele provádějí složitým procesem jako posloupností kroků nebo která obsahují komplexní doménové modely. Model zápisu má kompletní zásobník pro zpracování příkazů s obchodní logikou, ověřováním vstupu a obchodním ověřením. Model zápisu může považovat sadu přidružených objektů za jedinou jednotku pro změny dat (agregaci v terminologii DDD) a zajistit, aby tyto objekty byly vždy v konzistentním stavu. Model pro čtení nemá žádnou obchodní logiku ani zásobník ověřování a jen vrátí DTO pro použití v modelu zobrazení. Model čtení má konečnou konzistenci s modelem zápisu.

  • Scénáře, kdy musí být výkon čtení dat vyladěn odděleně od výkonu zápisů dat, zejména v případě, že je počet čtení mnohem větší než počet zápisů. V tomto scénáři můžete škálovat model čtení, ale spustit model zápisu jen na několika instancích. Malý počet instancí modelu zápisu také pomáhá minimalizovat výskyt konfliktů sloučení.

  • Scénáře, ve kterých se jeden tým vývojářů může zaměřit na komplexní doménový model, který je součástí modelu zápisu, a druhý tým se může zaměřit na model čtení a uživatelská rozhraní

  • Scénáře, ve kterých se očekává, že systém se v průběhu času vyvíjí a může obsahovat více verzí modelu, nebo ve kterých se obchodní pravidla běžně mění

  • Integrace s jinými systémy, zejména v kombinaci s modelem Event Sourcing, při které by dočasné selhání jednoho subsystému nemělo ovlivnit dostupnost ostatních subsystémů

Tento vzor se nedoporučuje v těchto případech:

  • Doména nebo obchodní pravidla jsou jednoduchá.

  • Stačí jednoduché uživatelské rozhraní ve stylu CRUD a operace přístupu k datům.

Zvažte možnost omezeného použití modelu CQRS jenom v těch částech vašeho systému, ve kterých to bude nejvýhodnější.

Návrh úloh

Architekt by měl vyhodnotit způsob použití modelu CQRS v návrhu úloh k řešení cílů a principů popsaných v pilířích architektury Azure Well-Architected Framework. Příklad:

Pilíř Jak tento model podporuje cíle pilíře
Efektivita výkonu pomáhá vaší úloze efektivně splňovat požadavky prostřednictvím optimalizací škálování, dat a kódu. Oddělení operací čtení a zápisu ve vysokých úlohách čtení a zápisu umožňuje cílené optimalizace výkonu a škálování pro konkrétní účel každé operace.

- PE:05 Škálování a dělení
- Výkon dat PE:08

Stejně jako u jakéhokoli rozhodnutí o návrhu zvažte jakékoli kompromisy proti cílům ostatních pilířů, které by mohly být s tímto vzorem zavedeny.

Model Event Sourcing a CQRS

Model CQRS se často používá společně s modelem Event Sourcing. Systémy založené na modelu CQRS používají samostatné datové modely pro čtení a zápis, které jsou přizpůsobené pro příslušné úlohy a nacházejí se často ve fyzicky oddělených úložištích. Při použití s modelem Event Sourcing je úložiště událostí modelem zápisu a je oficiálním zdrojem informací. Model čtení v systému založeném na modelu CQRS poskytuje materializovaná zobrazení dat, obvykle jako vysoce nenormalizovaná zobrazení. Tato zobrazení jsou přizpůsobená pro daná rozhraní a zobrazují požadavky aplikace, což pomáhá maximalizovat výkon zobrazení i dotazování.

Použití streamu událostí pro úložiště pro zápis, nikoli skutečných dat v časovém bodě, předchází konfliktům aktualizací u jedné agregace a maximalizuje výkon a škálovatelnost. Pomocí událostí lze asynchronně generovat materializovaná zobrazení dat, která se používají k naplnění úložiště pro čtení.

Úložiště událostí je oficiální zdroj informací, a proto během vývoje systému nebo v případě potřeby změny modelu čtení je možné odstraněním materializovaných zobrazení a přehráním všech minulých událostí vytvořit novou reprezentaci aktuálního stavu. Materializovaná zobrazení představují vlastně trvalou mezipaměť dat, která je jen pro čtení.

Při použití modelu CQRS v kombinaci s modelem Event Sourcing zvažte následující skutečnosti:

  • Systémy založené na tomto modelu, stejně jako každý systém, ve kterém jsou úložiště pro zápis a čtení samostatná, mají jenom konečnou konzistenci. Mezi vygenerováním události a aktualizací úložiště dat bude určité zpoždění.

  • Tento model zvyšuje složitost, protože je nutné vytvořit kód, který aktivuje a zpracovává události a sestavuje nebo aktualizuje příslušná zobrazení nebo objekty, jež požadují dotazy nebo model čtení. Složitost modelu CQRS při jeho použití s modelem Event Sourcing může komplikovat úspěšnou implementaci a vyžaduje jiný přístup při navrhování systémů. Model Event Sourcing ale může usnadnit modelování domény a usnadňuje nová sestavení zobrazení nebo vytvoření nových zobrazení, protože záměr pro změny v datech se zachovává.

  • Generování materializovaných zobrazení pro použití v modelu čtení nebo v projekcích dat přehráním a zpracováním událostí pro určité entity nebo kolekce entit může vyžadovat značnou dobu zpracování a značné využití prostředků. Platí to hlavně v případech, kdy se vyžaduje sumace nebo analýza hodnot za dlouhá období, protože všechny přidružené události může být nutné prověřit. Tento problém můžete vyřešit implementací snímků dat v naplánovaných intervalech, například celkovým počtem výskytů konkrétní akce nebo aktuálním stavem entity.

Příklad vzoru CQRS

Následující část obsahuje několik fragmentů kódu z ukázky implementace modelu CQRS, ve které se pro modely čtení a zápisu používají různé definice. Rozhraní tohoto modelu nevyžadují žádné součásti základních úložišť dat a dají se vyvíjet a vylaďovat nezávisle, protože tato rozhraní jsou oddělená.

Následující kód obsahuje definici modelu čtení.

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

Systém umožňuje uživatelům hodnocení produktů. V aplikaci se k tomu používá příkaz RateProduct, který se nachází v následujícím kódu.

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

Systém zpracovává příkazy odeslané aplikací pomocí třídy ProductsCommandHandler. Klienti odesílají příkazy do domény obvykle prostřednictvím systému zasílání zpráv, jako je třeba fronta. Obslužná rutina příkazu přijímá tyto příkazy a volá metody pro rozhraní domény. Úroveň podrobností každého příkazu je navržená tak, aby se omezil výskyt konfliktních požadavků. Následující kód ukazuje osnovu třídy 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)
  {
    ...
  }
}

Další kroky

Při implementaci tohoto modelu můžou být užitečné následující modely a pokyny:

  • Úvod do konzistence dat. Popisuje problémy, ke kterým obvykle při použití modelu CQRS dochází kvůli konečné konzistenci mezi úložišti dat pro čtení a zápis, a způsoby, jak se tyto problémy dají odstranit.

  • Horizontální, svislé a funkční dělení dat Popisuje osvědčené postupy pro rozdělení dat na oddíly, které je možné spravovat a přistupovat samostatně, aby se zlepšila škálovatelnost, snížila kolize a optimalizovala výkon.

  • Průvodci na cestě problematikou CQRS vám budou modely a postupy. Konkrétně představujeme model oddělení odpovědnosti příkazového dotazu, který se zkoumá a kdy je užitečný, a Epilogue: Poznatky vám pomůžou pochopit některé problémy, které se objeví při používání tohoto vzoru.

Blogové příspěvky Martina Fowlera:

  • Model Event Sourcing: Podrobně popisuje použití modelu Event Sourcing s modelem CQRS k tomu, aby se zjednodušily úlohy v komplexních doménách a současně aby se zlepšily výkon, škálovatelnost a rychlost odezvy. Dále popisuje, jak se dá zajistit konzistence transakčních dat a současně zachovat úplné záznamy pro audit a historii, aby bylo možné provádět kompenzační akce.

  • Model Materializované zobrazení. Model čtení v implementaci modelu CQRS může obsahovat materializovaná zobrazení dat modelu zápisu, případně lze ke generování materializovaných zobrazení daný model čtení použít.

  • Prezentace s lepším CQRS prostřednictvím vzorů asynchronní interakce uživatelů