Uwaga
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Podział odpowiedzialności zapytań poleceń (CQRS) to wzorzec projektowy, który oddziela operacje odczytu i zapisu dla magazynu danych na oddzielne modele danych. Takie podejście umożliwia niezależne optymalizowanie każdego modelu i może zwiększyć wydajność, skalowalność i bezpieczeństwo aplikacji.
Kontekst i problem
W tradycyjnej architekturze pojedynczy model danych jest często używany zarówno na potrzeby operacji odczytu, jak i zapisu. Takie podejście jest proste i jest odpowiednie dla podstawowych operacji tworzenia, odczytu, aktualizacji i usuwania (CRUD).
W miarę zwiększania się aplikacji coraz trudniej jest zoptymalizować operacje odczytu i zapisu w jednym modelu danych. Operacje odczytu i zapisu często mają różne wymagania dotyczące wydajności i skalowania. Tradycyjna architektura CRUD nie uwzględnia tej asymetrii, co może spowodować następujące wyzwania:
Niezgodność danych: Reprezentacje odczytu i zapisu danych często się różnią. Niektóre pola wymagane podczas aktualizacji mogą być niepotrzebne podczas operacji odczytu.
Rywalizacja o blokadę: Operacje równoległe w tym samym zestawie danych mogą powodować rywalizację o blokadę.
Problemy z wydajnością: 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.
Wyzwania związane z zabezpieczeniami: Zarządzanie zabezpieczeniami może być trudne, gdy jednostki podlegają operacjom odczytu i zapisu. To nakładanie się może uwidaczniać dane w niezamierzonych kontekstach.
Połączenie tych obowiązków może skutkować zbyt skomplikowanym modelem.
Rozwiązanie
Użyj wzorca CQRS, aby oddzielić operacje zapisu lub polecenia od operacji odczytu lub zapytań. Polecenia aktualizują dane. Zapytania pobierają dane. Wzorzec CQRS jest przydatny w scenariuszach, które wymagają wyraźnego rozdzielenia między poleceniami i odczytami.
Zrozum polecenia. Polecenia powinny reprezentować określone zadania biznesowe zamiast aktualizacji danych niskiego poziomu. Na przykład w aplikacji hotel-booking użyj polecenia "Book hotel room" zamiast "Set ReservationStatus to Reserved". Takie podejście lepiej przechwytuje intencję użytkownika i dostosowuje polecenia do procesów biznesowych. Aby zapewnić pomyślne wykonanie poleceń, może być konieczne udoskonalenie przepływu interakcji użytkownika i logiki po stronie serwera oraz rozważenie przetwarzania asynchronicznego.
Obszar doskonalenia Rekomendacja Walidacja po stronie klienta Zweryfikuj określone warunki przed wysłaniem polecenia, aby zapobiec oczywistym niepowodzeniom. Jeśli na przykład żadne pokoje nie są dostępne, wyłącz przycisk "Rezerwuj" i podaj jasny i przyjazny komunikat dla użytkownika w UI, który wyjaśnia, dlaczego rezerwacja nie jest możliwa. Ta konfiguracja zmniejsza niepotrzebne żądania serwera i zapewnia użytkownikom natychmiastową informację zwrotną, co poprawia ich doświadczenia. Logika po stronie serwera Zwiększ logikę biznesową, aby bezpiecznie obsługiwać przypadki brzegowe i błędy. Aby na przykład rozwiązać warunki wyścigu, takie jak wielu użytkowników próbujących zarezerwować ostatni dostępny pokój, rozważ dodanie użytkowników do listy oczekujących lub sugerowanie alternatyw. Przetwarzanie asynchroniczne Przetwarzaj polecenia asynchronicznie , umieszczając je w kolejce, zamiast obsługiwać je synchronicznie. Omówienie zapytań. Zapytania nigdy nie zmieniają danych. Zamiast tego zwracają obiekty transferu danych (DTO), które przedstawiają wymagane dane w wygodnym formacie bez żadnej logiki domeny. To odrębne rozdzielenie obowiązków upraszcza projektowanie i wdrażanie systemu.
Oddzielne modele odczytu i modele zapisu
Oddzielenie modelu odczytu od modelu zapisu upraszcza projektowanie i implementację systemu przez rozwiązanie konkretnych problemów dotyczących zapisów danych i odczytów danych. Ta separacja zwiększa przejrzystość, skalowalność i wydajność, ale wprowadza kompromisy. Na przykład narzędzia do tworzenia szkieletów, takie jak struktury mapowania obiektów (O/RM), nie mogą automatycznie generować kodu CQRS ze schematu bazy danych, więc potrzebujesz niestandardowej logiki, aby wypełnić lukę.
W poniższych sekcjach opisano dwa podstawowe podejścia do implementowania separacji modelu odczytu i zapisu w usługach CQRS. Każde podejście ma unikatowe korzyści i wyzwania, takie jak zarządzanie synchronizacją i spójnością.
Oddzielanie modeli w jednym magazynie danych
Takie podejście reprezentuje podstawowy poziom CQRS, w którym modele odczytu i zapisu współużytkują pojedynczą bazową bazę danych, ale utrzymują odrębną logikę dla swoich operacji. Podstawowa architektura CQRS umożliwia oddzielenie modelu zapisu od modelu odczytu podczas korzystania z udostępnionego magazynu danych.
Takie podejście zwiększa przejrzystość, wydajność i skalowalność, definiując odrębne modele do obsługi problemów z odczytem i zapisem.
Model zapisu jest przeznaczony do obsługi poleceń aktualizujących lub utrwalających dane. Obejmuje ona walidację i logikę domeny oraz pomaga zapewnić spójność danych dzięki optymalizacji pod kątem integralności transakcyjnej i procesów biznesowych.
Model odczytu jest przeznaczony do obsługi zapytań dotyczących pobierania danych. Koncentruje się on na generowaniu obiektów DTOs lub projekcji zoptymalizowanych pod kątem warstwy prezentacji. Zwiększa wydajność zapytań i czas odpowiedzi, unikając logiki domeny.
Oddzielne modele w różnych magazynach danych
Bardziej zaawansowana implementacja CQRS używa odrębnych magazynów danych dla modeli odczytu i zapisu. Rozdzielenie magazynów danych odczytu i zapisu umożliwia skalowanie poszczególnych modeli w celu dopasowania ich do obciążenia. Umożliwia również korzystanie z innej technologii magazynowania dla każdego magazynu danych. Możesz użyć bazy danych dokumentów dla magazynu danych odczytu i relacyjnej bazy danych dla magazynu danych zapisu.
W przypadku korzystania z oddzielnych magazynów danych należy upewnić się, że obie te magazyny pozostają zsynchronizowane. Typowym wzorcem jest to, że model zapisu publikuje zdarzenia podczas aktualizowania bazy danych, które model odczytu wykorzystuje do odświeżania swoich danych. Aby uzyskać więcej informacji na temat używania zdarzeń, zobacz Styl architektury opartej na zdarzeniach. Ponieważ zwykle nie można zarejestrować brokerów komunikatów i baz danych w jednej transakcji rozproszonej, problemy ze spójnością mogą wystąpić podczas aktualizowania bazy danych i zdarzeń publikowania. Aby uzyskać więcej informacji, zobacz Idempotentne przetwarzanie komunikatów.
Magazyn danych odczytu może używać własnego schematu danych zoptymalizowanego pod kątem zapytań. Może na przykład przechowywać zmaterializowany widok danych, aby uniknąć złożonych sprzężeń lub mapowań O/RM. Magazyn danych odczytu może być repliką magazynu zapisu przeznaczoną wyłącznie do odczytu lub mieć inną strukturę. Wdrażanie wielu replik tylko do odczytu może zwiększyć wydajność, zmniejszając opóźnienia i zwiększając dostępność, szczególnie w scenariuszach rozproszonych.
Zalety CQRS
Niezależne skalowanie. Usługa CQRS umożliwia niezależne skalowanie modeli odczytu i zapisu. Takie podejście może pomóc zminimalizować rywalizację o blokadę i zwiększyć wydajność systemu pod obciążeniem.
Zoptymalizowane schematy danych. Operacje odczytu mogą używać schematu zoptymalizowanego pod kątem zapytań. Operacje zapisu używają schematu zoptymalizowanego pod kątem aktualizacji.
Bezpieczeństwo. Oddzielając odczyty i zapisy, można upewnić się, że tylko odpowiednie jednostki domeny lub operacje mają uprawnienia do wykonywania akcji zapisu na danych.
Separacja obaw. Rozdzielenie obowiązków odczytu i zapisu skutkuje czystszymi, bardziej konserwowalnymi modelami. Strona zapisu zwykle obsługuje złożoną logikę biznesową. Strona odczytu może pozostać prosta i skoncentrowana na wydajności zapytań.
Prostsze zapytania. Podczas przechowywania zmaterializowanego widoku w bazie danych odczytu aplikacja może uniknąć złożonych sprzężeń podczas wykonywania zapytań.
Problemy i zagadnienia
Podczas podejmowania decyzji o zaimplementowaniu tego wzorca należy wziąć pod uwagę następujące kwestie:
Zwiększona złożoność. Podstawowa koncepcja CQRS jest prosta, ale może wprowadzić znaczną złożoność w projekcie aplikacji, szczególnie w połączeniu ze wzorcem określania źródła zdarzeń.
Wyzwania związane z obsługą komunikatów. Obsługa komunikatów nie jest wymagana w przypadku usług CQRS, ale często służy do przetwarzania poleceń i publikowania zdarzeń aktualizacji. Podczas dołączania komunikatów system musi uwzględniać potencjalne problemy, takie jak błędy komunikatów, duplikaty i ponawianie prób. Aby uzyskać więcej informacji na temat strategii obsługi poleceń o różnych priorytetach, zobacz Kolejki priorytetów.
Spójność ostateczna. Gdy bazy danych odczytu i bazy danych zapisu są oddzielone, dane odczytu mogą nie odzwierciedlać najnowszych zmian natychmiastowo. To opóźnienie powoduje nieaktualne dane. Zapewnienie, że magazyn modeli odczytu pozostaje up-to-date ze zmianami w magazynie modeli zapisu może być trudny. Ponadto wykrywanie i obsługa scenariuszy, w których użytkownik działa na nieaktualnych danych, wymaga starannego rozważenia.
Kiedy należy używać tego wzorca
Użyj tego wzorca, gdy:
Pracujesz w środowiskach współpracy. W środowiskach, w których wielu użytkowników uzyskuje dostęp do tych samych danych i modyfikuje je jednocześnie, usługa CQRS pomaga zmniejszyć konflikty scalania. Polecenia mogą obejmować wystarczającą stopień szczegółowości, aby zapobiec konfliktom, a system może rozwiązać wszelkie konflikty występujące w logice poleceń.
Masz interfejsy użytkownika oparte na zadaniach. Aplikacje, które prowadzą użytkowników przez złożone procesy jako serię kroków lub złożonych modeli domen, korzystają z CQRS.
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, która jest znana jako agregacja w terminologii projektowej opartej na domenie. Model zapisu może również pomóc w zapewnieniu, że te obiekty są zawsze w stanie spójnym.
Model odczytu nie ma logiki biznesowej ani stosu walidacji. Zwraca obiekt DTO do użycia w modelu widoku. Model odczytu jest ostatecznie spójny z modelem zapisu.
Potrzebujesz dostrajania wydajności. Systemy, w których wydajność odczytów danych musi być dostrojona oddzielnie od wydajności zapisów danych, korzystają z CQRS. Ten wzorzec jest szczególnie korzystny, gdy liczba odczytów jest większa niż liczba zapisów. Model odczytu jest skalowany w poziomie w celu obsługi dużych woluminów zapytań. Model zapisu działa na mniejszej liczbie wystąpień, aby zminimalizować konflikty scalania i zachować spójność.
Masz separację problemów związanych z rozwojem. Usługa CQRS umożliwia zespołom niezależnie pracę. Jeden zespół implementuje złożoną logikę biznesową w modelu zapisu, a inny zespół opracowuje składniki modelu odczytu i interfejsu użytkownika.
Masz ewoluujące systemy. Usługa CQRS obsługuje systemy, które ewoluują wraz z upływem czasu. Uwzględnia nowe wersje modelu, częste zmiany reguł biznesowych lub inne modyfikacje bez wpływu na istniejące funkcje.
Potrzebna jest integracja systemu: Systemy zintegrowane z innymi podsystemami, zwłaszcza systemy korzystające ze wzorca określania źródła zdarzeń, pozostają dostępne nawet wtedy, gdy podsystem tymczasowo ulegnie awarii. Usługa CQRS izoluje awarie, co zapobiega wpływowi pojedynczego składnika na cały system.
Ten wzorzec może nie być odpowiedni w następujących przypadkach:
Domena lub reguły biznesowe są proste.
Wystarczy prosty interfejs użytkownika w stylu CRUD i operacje dostępu do danych.
Projektowanie obciążenia
Oceń, jak używać wzorca CQRS w projekcie obciążenia, aby sprostać celom i zasadom opisanym w filarach platformy Azure Well-Architected Framework. Poniższa tabela zawiera wskazówki dotyczące tego, jak ten wzorzec obsługuje cele filaru Wydajność.
Filar | Jak ten wzorzec obsługuje cele filaru |
---|---|
Efektywność wydajności pomaga wydajnie sprostać wymaganiom dzięki optymalizacjom skalowania, danych i kodu. | Rozdzielenie operacji odczytu i operacji zapisu w dużych obciążeniach odczytu do zapisu umożliwia ukierunkowaną wydajność i optymalizacje skalowania dla konkretnego celu każdej operacji. - PE:05 Skalowanie i partycjonowanie - PE:08 Wydajność danych |
Rozważ wszelkie kompromisy w kontekście celów innych filarów, które może wprowadzić ten model.
Łączenie wzorców określania źródła zdarzeń i CQRS
Niektóre implementacje CQRS zawierają wzorzec określania źródła zdarzeń. Ten wzorzec przechowuje stan systemu jako chronologiczną serię zdarzeń. Każde zdarzenie przechwytuje zmiany wprowadzone w danych w określonym czasie. Aby określić bieżący stan, system odtwarza te zdarzenia w kolejności. W tej konfiguracji:
Magazyn zdarzeń to model zapisu i pojedyncze źródło prawdy.
Model odczytu generuje zmaterializowane widoki z tych zdarzeń, zazwyczaj w wysoce zdenormalizowanej formie. Te widoki optymalizują pobieranie danych, dostosowując struktury do wymagań dotyczących zapytań i wyświetlania.
Zalety łączenia wzorców określania źródła zdarzeń i CQRS
Te same zdarzenia, które aktualizują model zapisu, mogą służyć jako dane wejściowe do modelu odczytu. Model odczytu może następnie utworzyć migawkę bieżącego stanu w czasie rzeczywistym. Te migawki optymalizują zapytania, zapewniając wydajne i wstępnie skompilowane widoki danych.
Zamiast bezpośrednio przechowywać bieżący stan, system używa strumienia zdarzeń jako magazynu zapisu. Takie podejście zmniejsza konflikty aktualizacji w agregacjach i zwiększa wydajność i skalowalność. System może przetwarzać te zdarzenia asynchronicznie, aby tworzyć lub aktualizować zmaterializowane widoki przeznaczone do odczytu z magazynu danych.
Ponieważ magazyn zdarzeń działa jako pojedyncze źródło prawdy, można łatwo ponownie wygenerować zmaterializowane widoki lub dostosować się do zmian w modelu odczytu, odtwarzając zdarzenia historyczne. Zasadniczo zmaterializowane widoki działają jako trwała pamięć podręczna tylko do odczytu zoptymalizowana pod kątem szybkich i wydajnych zapytań.
Zagadnienia dotyczące łączenia wzorców określania źródła zdarzeń i CQRS
Przed połączeniem wzorca CQRS ze wzorcem określania źródła zdarzeń należy ocenić następujące kwestie:
Spójność ostateczna: Ponieważ magazyny danych zapisu i odczytu są oddzielne, aktualizacje magazynu danych odczytu mogą opóźnić generowanie zdarzeń. To opóźnienie powoduje spójność ostateczną.
Zwiększona złożoność: Połączenie wzorca CQRS ze wzorcem określania źródła zdarzeń wymaga innego podejścia projektowego, co może utrudnić pomyślną implementację. Musisz napisać kod w celu generowania, przetwarzania i obsługi zdarzeń oraz tworzenia lub aktualizowania widoków dla modelu odczytu. Jednak wzorzec określania źródła zdarzeń upraszcza modelowanie domeny i umożliwia łatwe kompilowanie lub tworzenie nowych widoków przez zachowanie historii i intencji wszystkich zmian danych.
Wydajność generowania widoku: Generowanie zmaterializowanych widoków dla modelu odczytu może zużywać dużo czasu i zasobów. To samo dotyczy projekcji danych przez odtworzenie i przetwarzanie zdarzeń dla określonych jednostek lub kolekcji. Złożoność zwiększa się, gdy obliczenia obejmują analizowanie lub sumowanie wartości w długich okresach, ponieważ należy zbadać wszystkie powiązane zdarzenia. Implementowanie migawek danych w regularnych odstępach czasu. Na przykład przechowuj bieżący stan obiektu lub okresowe migawki zagregowanych sum, które wskazują, ile razy występuje określona akcja. Migawki zmniejszają konieczność wielokrotnego przetwarzania pełnej historii zdarzeń, co zwiększa efektywność.
Przykład
Poniższy kod pokazuje fragmenty z przykładu implementacji CQRS, która korzysta z różnych definicji dla modeli odczytu i modeli zapisu. Interfejsy modelu nie określają funkcji bazowych magazynów danych i mogą ewoluować i być dostosowane niezależnie, ponieważ te interfejsy są oddzielne.
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 wykonuje to przy użyciu RateProduct
polecenia 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 ProductsCommandHandler
klasy 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ępny krok
Podczas implementowania tego wzorca mogą być istotne następujące informacje:
- Wskazówki dotyczące partycjonowania danych opisują najlepsze rozwiązania dotyczące dzielenia danych na partycje, którymi można zarządzać i uzyskiwać do ich dostępu oddzielnie, aby zwiększyć skalowalność, zmniejszyć rywalizację i zoptymalizować wydajność.
Powiązane zasoby
Wzorzec określania źródła zdarzeń. W tym wzorcu opisano, jak uprościć zadania w złożonych domenach i zwiększyć wydajność, skalowalność i czas odpowiedzi. Wyjaśniono również, jak zapewnić spójność danych transakcyjnych przy zachowaniu pełnych dzienników inspekcji i historii, które mogą umożliwić akcje wyrównywujące.
Materialized View pattern (Wzorzec zmaterializowanego widoku). Ten wzorzec tworzy wstępnie wypełniane widoki, znane jako zmaterializowane widoki, w celu wydajnego wykonywania zapytań i wyodrębniania danych z co najmniej jednego magazynu danych. Model odczytu implementacji CQRS może zawierać zmaterializowane widoki danych modelu zapisu lub może on służyć do generowania zmaterializowanych widoków.