Wzorzec CQRS

Storage

CQRS to segregacja poleceń i odpowiedzialności zapytań, wzorzec oddzielający operacje odczytu i aktualizacji magazynu danych. Zaimplementowanie magazynu CQRS w aplikacji może zmaksymalizować jej wydajność, skalowalność i zabezpieczenia. Elastyczność utworzona przez migrację do magazynu CQRS umożliwia systemowi lepsze rozwijanie się wraz z upływem czasu i uniemożliwia aktualizowanie poleceń powodujących konflikty scalania na poziomie domeny.

Kontekst i problem

W tradycyjnych architekturach ten sam model danych jest używany do wysyłania zapytań do bazy danych i aktualizowania jej. Jest to proste i dobrze się sprawdza w przypadku podstawowych operacji CRUD. Jednak w bardziej złożonych aplikacjach ta metoda może stać się niewydolna. Na przykład po stronie odczytu aplikacja może wykonywać wiele różnych zapytań zwracających obiekty transferu danych (DTO, data transfer object) o różnych kształtach. Mapowanie obiektu może stać się skomplikowane. Po stronie zapisu model może wdrażać złożoną walidację i logikę biznesową. W efekcie możesz uzyskać zbyt skomplikowany model, który wykonuje zbyt dużo działań.

Obciążenia odczytu i zapisu są często asymetryczne, z bardzo różnymi wymaganiami dotyczącymi wydajności i skalowania.

Tradycyjna architektura CRUD

  • Często występuje niezgodność między reprezentacjami odczytu i zapisu danych, takimi jak dodatkowe kolumny lub właściwości, które muszą zostać poprawnie zaktualizowane, mimo że nie są wymagane w ramach operacji.

  • Rywalizacja o dane może wystąpić, gdy operacje są wykonywane równolegle na tym samym zestawie danych.

  • Tradycyjne podejście może mieć negatywny wpływ na wydajność ze względu na obciążenie magazynu danych i warstwy dostępu do danych oraz złożoność zapytań wymaganych do pobrania informacji.

  • Zarządzanie zabezpieczeniami i uprawnieniami może stać się złożone, ponieważ każda jednostka podlega operacjom odczytu i zapisu, które mogą uwidaczniać dane w niewłaściwym kontekście.

Rozwiązanie

Magazyn CQRS oddziela odczyty i zapisy w różnych modelach , używając poleceń do aktualizowania danych i zapytań w celu odczytu danych.

  • Polecenia powinny być oparte na zadaniach, a nie skoncentrowane na danych. ("Zarezerwuj pokój hotelowy", a nie "ustaw wartość ReservationStatus na Reserved").
  • Polecenia mogą być umieszczane w kolejce do przetwarzania asynchronicznego, a nie przetwarzane synchronicznie.
  • Zapytania nigdy nie modyfikują bazy danych. Zapytanie zwraca obiekt DTO, który nie hermetyzuje żadnej wiedzy domeny.

Modele można następnie odizolować, jak pokazano na poniższym diagramie, chociaż nie jest to bezwzględne wymaganie.

Podstawowa architektura CQRS

Posiadanie oddzielnych modeli zapytań i aktualizacji upraszcza projektowanie i implementację. Jednak jedną wadą jest to, że kod CQRS nie może być generowany automatycznie na podstawie schematu bazy danych przy użyciu mechanizmów tworzenia szkieletu, takich jak narzędzia O/RM.

Aby uzyskać większą izolację, możesz fizycznie oddzielić odczyt danych od zapisu danych. W takim przypadku baza danych odczytu może używać własnego schematu danych zoptymalizowanego dla zapytań. Na przykład może ona przechowywać zmaterializowany widok danych, aby uniknąć złożonych połączeń lub złożonych mapowań obiektowo-relacyjnych. Może ona nawet używać innego typu magazynu danych. Na przykład baza danych zapisu może być relacyjna, natomiast baza danych odczytu może być bazą danych dokumentów.

Jeśli są używane oddzielne bazy danych odczytu i zapisu, muszą być zsynchronizowane. Zazwyczaj jest to realizowane przez opublikowanie zdarzenia przez opublikowanie modelu zapisu za każdym razem, gdy aktualizuje bazę danych. Aby uzyskać więcej informacji na temat używania zdarzeń, zobacz Styl architektury opartej na zdarzeniach. Aktualizacja bazy danych i publikowanie zdarzenia muszą nastąpić w ramach jednej transakcji.

Architektura CQRS z oddzielnymi magazynami odczytu i zapisu

Magazyn odczytu może być repliką tylko do odczytu magazynu zapisu lub magazyny odczytu i zapisu mogą mieć całkowicie inną strukturę. Użycie wielu replik tylko do odczytu może zwiększyć wydajność zapytań, szczególnie w scenariuszach rozproszonych, w których repliki tylko do odczytu znajdują się blisko wystąpień aplikacji.

Rozdzielenie magazynów odczytu i zapisu zapewnia każdemu z nich możliwość odpowiedniego skalowania w celu dopasowania do obciążenia. Na przykład magazyny odczytu są zazwyczaj znacznie bardziej obciążone niż magazyny zapisu.

Niektóre implementacje podejścia CQRS używają wzorca określania źródła zdarzeń. Za pomocą tego wzorca stan aplikacji jest przechowywany jako sekwencja zdarzeń. Każde zdarzenie reprezentuje zestaw zmian danych. Bieżący stan jest tworzony przez ponowne odtwarzanie zdarzeń. W kontekście CQRS jedną z zalet określania źródła zdarzeń jest to, że te same zdarzenia mogą służyć do powiadamiania innych składników — w szczególności do powiadamiania modelu odczytu. Model odczytu używa zdarzeń do tworzenia migawki bieżącego stanu, co jest bardziej wydajne dla zapytań. Jednak określanie źródła zdarzeń zwiększa złożoność projektu.

Zalety magazynu CQRS obejmują:

  • Niezależne skalowanie. Podejście CQRS umożliwia niezależne skalowanie obciążeń odczytu oraz zapisu i może skutkować mniejszą liczbą blokad rywalizacji.
  • Zoptymalizowane schematy danych. Strona odczytu może używać schematu zoptymalizowanego dla zapytań, a strona zapisu — schematu zoptymalizowanego dla aktualizacji.
  • Bezpieczeństwo. Łatwiej jest zapewnić możliwość wykonywania operacji zapisu względem danych tylko przez właściwe jednostki domeny.
  • Separacja problemów. Oddzielenie stron odczytu i zapisu może doprowadzić do modeli, które są łatwiejsze w obsłudze i bardziej elastyczne. Większość złożonej logiki biznesowej przechodzi do modelu zapisu. Model odczytu może być stosunkowo prosty.
  • Prostsze zapytania. Zapisując zmaterializowany widok w bazie danych odczytu, aplikacja może uniknąć złożonych połączeń podczas wykonywania zapytania.

Problemy i zagadnienia dotyczące implementacji

Oto niektóre wyzwania związane z wdrażaniem tego wzorca:

  • Złożoność. Podstawowa koncepcja CQRS jest prosta. Jednak może ona prowadzić do bardziej złożonego projektu aplikacji zwłaszcza wtedy, gdy zawiera wzorzec określania źródła zdarzeń.

  • Obsługa komunikatów. Chociaż podejście CQRS nie wymaga obsługi komunikatów, jest ona często używana do przetwarzania poleceń i publikowania zdarzeń aktualizacji. W takim przypadku aplikacja musi obsługiwać błędy komunikatów lub zduplikowane komunikaty. Zapoznaj się ze wskazówkami dotyczącymi kolejek priorytetowych , aby radzić sobie z poleceniami o różnych priorytetach.

  • Spójność ostateczna. Jeśli oddzielisz bazy danych odczytu i zapisu, dane odczytu mogą być nieaktualne. Magazyn modeli odczytu musi zostać zaktualizowany w celu odzwierciedlenia zmian w magazynie modeli zapisu i może być trudno wykryć, kiedy użytkownik wystawił żądanie na podstawie nieaktualnych danych odczytu.

Kiedy używać wzorca CQRS

Rozważ użycie magazynu CQRS w następujących scenariuszach:

  • Domeny współpracy, w których wielu użytkowników uzyskuje dostęp do tych samych danych równolegle. CQRS umożliwia definiowanie poleceń z wystarczającą szczegółowością, aby zminimalizować konflikty scalania na poziomie domeny, a konflikty, które występują, można scalić za pomocą polecenia .

  • Oparte na zadaniach interfejsy użytkownika, gdzie użytkownicy są prowadzeni przez złożony proces w szeregu kroków lub za pomocą złożonych modeli domeny. Model zapisu ma pełny stos przetwarzania poleceń z logiką biznesową, walidacją danych wejściowych i weryfikacją biznesową. Model zapisu może traktować zestaw skojarzonych obiektów jako pojedynczą jednostkę zmian danych (agregację w terminologii DDD) i upewnić się, że te obiekty są zawsze w stanie spójnym. Model odczytu nie ma logiki biznesowej ani stosu weryfikacji i po prostu zwraca cel DTO do użycia w modelu widoku. Model odczytu jest ostatecznie spójny z modelem zapisu.

  • Scenariusze, w których wydajność odczytów danych musi być dostrojona oddzielnie od wydajności zapisów danych, zwłaszcza gdy liczba odczytów jest znacznie większa niż liczba zapisów. W tym scenariuszu można skalować model odczytu w poziomie, ale uruchamiać model zapisu na kilka wystąpień. Mała liczba wystąpień modelu zapisu pomaga też w zminimalizowaniu występowania konfliktów scalania.

  • Scenariusze, w których jeden zespół deweloperów może skupić się na złożonym modelu domeny będącym częścią modelu zapisu, a inny zespół może skupić się na modelu odczytu i interfejsach użytkownika.

  • Scenariusze, w których system ma podlegać ewolucji w czasie i może zawierać wiele wersji modelu lub w których reguły biznesowe są regularnie zmieniane.

  • Integracja z innymi systemami, zwłaszcza w połączeniu z określaniem źródła zdarzeń, gdzie błąd czasowy jednego podsystemu nie powinien wpływać na dostępność innych.

Ten wzorzec nie jest zalecany, gdy:

  • Domena lub reguły biznesowe są proste.

  • Wystarczy prosty interfejs użytkownika w stylu CRUD i operacje dostępu do danych.

Rozważ stosowanie podejścia CQRS do ograniczonych sekcji systemu, gdzie będzie ono najbardziej przydatne.

Określanie źródła zdarzeń i wzorzec CQRS

Wzorzec CQRS jest często używany wraz ze wzorcem określania źródła zdarzeń. Systemy oparte na wzorcu CQRS używają oddzielnych modeli odczytu i zapisu danych, a każdy z nich jest dostosowany do odpowiednich zadań i często znajduje się w fizycznie oddzielonych magazynach. W przypadku użycia ze wzorcem określania źródła zdarzeń magazyn zdarzeń jest modelem zapisu i jest oficjalnym źródłem informacji. Model odczytu systemu opartego na podejściu CQRS udostępnia zmaterializowane widoki danych zwykle jako widoki w wysokim stopniu zdenormalizowane. Widoki te są dostosowane do interfejsów i wyświetlają wymagania dotyczące aplikacji, które pomagają w zmaksymalizowaniu wydajności zarówno wyświetlania, jak i zapytań.

Używanie strumienia zdarzeń jako magazynu zapisu, a nie rzeczywistych danych w punkcie w czasie, pozwala uniknąć konfliktów aktualizacji dla jednej wartości zagregowanej oraz maksymalizuje wydajność i skalowalność. Zdarzenia mogą służyć do asynchronicznego generowania zmaterializowanych widoków danych, które są używane do zapełnienia magazynu odczytu.

Ponieważ magazyn zdarzeń jest oficjalnym źródłem informacji, istnieje możliwość usunięcia zmaterializowanych widoków i odtworzenia wszystkich wcześniejszych zdarzeń, aby utworzyć nową reprezentację bieżącego stanu, gdy system ewoluuje lub gdy model odczytu należy zmienić. Zmaterializowane widoki są w zasadzie trwałą pamięcią podręczną umożliwiającą tylko odczyt danych.

W razie używania podejścia CQRS w połączeniu z wzorcem określania źródła zdarzeń należy rozważyć następujące kwestie:

  • Podobnie jak w przypadku każdego systemu, gdzie magazyny zapisu i odczytu są oddzielone, systemy oparte na tym wzorcu są spójne tylko ostatecznie. Będzie istnieć pewne opóźnienie między generowanym zdarzeniem i aktualizowanym magazynem danych.

  • Wzorzec zwiększa złożoność, ponieważ musi zostać utworzony kod do inicjowania i obsługi zdarzeń oraz łączenia albo aktualizowania odpowiednich widoków lub obiektów wymaganych przez zapytania lub modele odczytu. Złożoność wzorca CQRS, gdy jest on używany ze wzorcem określania źródła zdarzeń, może utrudnić pomyślne wdrożenie i wymaga innego podejścia do projektowania systemów. Jednak określanie źródła zdarzeń może ułatwić modelowanie domeny i uprościć przebudowę widoków lub tworzenie nowych, ponieważ cel zmian w danych jest zachowany.

  • Generowanie zmaterializowanych widoków do użycia w modelu odczytu lub projekcji danych przez odtwarzanie i obsługę zdarzeń dla określonych obiektów lub kolekcji obiektów może wymagać znacznego czasu przetwarzania i wykorzystania zasobów. Dotyczy to zwłaszcza sytuacji, w których jest wymagane sumowanie lub analiza wartości w długim okresie, ponieważ może być konieczne zbadanie wszystkich skojarzonych zdarzeń. Rozwiąż ten problem, implementując migawki danych w zaplanowanych odstępach czasu, na przykład łączną liczbę określonej akcji, która wystąpiła, lub bieżący stan jednostki.

Przykład wzorca CQRS

Poniższy kod pokazuje pewne fragmenty przykładu wdrożenia podejścia CQRS, które używają różnych definicji modeli odczytu i zapisu. Interfejsy modelu nie wymagają żadnych funkcji podstawowych magazynów danych oraz mogą ewoluować i mogą być niezależnie strojone, ponieważ te interfejsy są rozdzielone.

Poniższy kod przedstawia definicję modelu odczytu.

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

System pozwala użytkownikom oceniać produkty. Kod aplikacji realizuje to przy użyciu polecenia RateProduct pokazanego w poniższym kodzie.

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

System używa klasy ProductsCommandHandler do obsługi poleceń wysyłanych przez aplikację. Klienci wysyłają zazwyczaj polecenia do domeny za pośrednictwem systemu obsługi komunikatów, takiego jak kolejka. Procedura obsługi poleceń akceptuje te polecenia i wywołuje metody interfejsu domeny. Stopień szczegółowości każdego polecenia pozwala zmniejszyć prawdopodobieństwo żądań powodujących konflikt. Poniższy kod przedstawia zarys klasy 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)
  {
    ...
  }
}

Następne kroki

Podczas implementowania tego wzorca są przydatne następujące wzorce i wskazówki:

Wpisy na blogu Martina Fowlera:

  • Wzorzec określania źródła zdarzeń. Bardziej szczegółowo opisuje sposób używania określania źródła zdarzeń ze wzorcem CQRS w celu uproszczenia zadań w złożonych domenach oraz jednoczesnego zwiększenia wydajności, skalowalności i elastyczności. Ponadto przedstawia sposób zapewniania spójności danych transakcyjnych przy zachowaniu pełnego dziennika inspekcji i historii umożliwiającej akcje kompensacyjne.

  • Zmaterializowany wzorzec widoku. Model odczytu implementacji CQRS może zawierać zmaterializowane widoki danych modelu zapisu lub może on służyć do generowania zmaterializowanych widoków.