Co to jest wzorzec CQRS?

CQRS to skrót od Command and Query Responsibility Segregation, który oddziela operacje odczytu i aktualizacji dla magazynu danych. Implementacja CQRS w aplikacji może zmaksymalizować jej wydajność, skalowalność i bezpieczeństwo. Elastyczność utworzona przez migrowanie do usługi CQRS umożliwia systemowi lepsze ewolucję wraz z czasem i zapobiega powodowaniu konfliktów scalania przez polecenia aktualizacji 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 w zakresie wydajności i skalowania.

Tradycyjna architektura CRUD

  • Często występuje niezgodność między reprezentacjami odczytu i zapisu danych, na przykład dodatkowymi kolumnami lub właściwościami, które muszą zostać poprawnie zaktualizowane, mimo że nie są wymagane w ramach operacji.

  • Skweda danych 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 zarówno operacji odczytu, jak i zapisu, co może spowodować uwidocznianie danych w niewłaściwym kontekście.

Rozwiązanie

Model CQRS oddziela odczyty i zapis w różnych modelach przy użyciu poleceń do aktualizowania danych i zapytań w celu odczytywania danych.

  • Polecenia powinny być oparte na zadaniach, a nie na danych. ("Book hotel room", a nie "set ReservationStatus to 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

Oddzielne modele zapytań i aktualizacji upraszczają projektowanie i implementację. Jednak jedną wadą jest to, że kod CQRS nie może być automatycznie generowany na podstawie schematu bazy danych przy użyciu mechanizmów tworzenia szkieletów, 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 używane są oddzielne bazy danych odczytu i zapisu, muszą być one zsynchronizowane. Zwykle jest to realizowane przez publikowanie zdarzenia za każdym razem, gdy model zapisu 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ę w pobliżu 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 korzyścią określania źródła zdarzeń jest to, że te same zdarzenia mogą być używane do powiadamiania innych składników — w szczególności 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 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

Niektóre wyzwania związane z implementacją tego wzorca to:

  • 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 na temat kolejek priorytetowych, aby mieć do czynienia 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 modelu zapisu. Wykrycie, kiedy użytkownik wystawił żądanie na podstawie nieaktualnych danych odczytu, może być trudne.

Kiedy używać wzorca CQRS

Należy wziąć pod uwagę CQRS dla następujących scenariuszy:

  • Domeny współpracy, w których wielu użytkowników równolegle uzyskuje dostęp do tych samych danych. Zasady CQRS umożliwiają definiowanie poleceń z wystarczającą szczegółowością, aby zminimalizować konflikty scalania na poziomie domeny, a konflikty, które się pojawiają, mogą zostać scalone 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ą, weryfikacją danych wejściowych i weryfikacją biznesową. Model zapisu może traktować zestaw skojarzonych obiektów jako pojedynczą jednostkę dla zmian danych (agregacja w terminologii DDD) i upewnić się, że te obiekty są zawsze w spójnym stanie. Model odczytu nie ma logiki biznesowej ani stosu walidacji i po prostu zwraca wartość DTO do użycia w modelu widoku. Model odczytu jest ostatecznie spójny z modelem zapisu.

  • Scenariusze, w których wydajność odczytu danych musi być dostrojona niezależnie od wydajności zapisu danych, szczególnie gdy liczba odczytów jest znacznie większa niż liczba odczytów. W tym scenariuszu można skalować model odczytu w zewnątrz, ale uruchomić model zapisu w zaledwie kilku wystąpieniach. 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 w przypadku:

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

Model sourcingu 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 korzystania ze wzorca źródłazdarzeń 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, takie jak łączna liczba 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:

  • Data Consistency Primer (Elementarz spójności danych). Opisuje problemy, które zazwyczaj występują z powodu spójności ostatecznej między magazynami danych odczytu i zapisu podczas używania wzorca CQRS, i sposoby rozwiązywania tych problemów.

  • Partycjonowanie danych poziomych, pionowych i funkcjonalnych. W tym artykule opisano najlepsze rozwiązania dotyczące dzielenia danych na partycje, które mogą być zarządzane i dostępne oddzielnie w celu zwiększenia skalowalności, zmniejszenia skwedycji i zoptymalizowania wydajności.

  • Przewodnik po wzorcach i rozwiązaniach CQRS Journey (Podróż CQRS). W szczególności wprowadzenie wzorca podziału odpowiedzialności zapytania poleceń eksploruje wzorzec i gdy jest on przydatny, a epilog: zdobyta lekcja pomaga zrozumieć niektóre problemy, które są związane z używaniem tego wzorca.

Wpisy w blogu Martina Fowlera:

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

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