Ponowne nawodnienie agregatów z projekcji „migawek” zamiast ze Sklepu zdarzeń

14

Od jakiegoś czasu flirtuję z Event Sourcing i CQRS, chociaż nigdy nie miałem okazji zastosować wzorców w prawdziwym projekcie.

Rozumiem korzyści płynące z rozdzielenia problemów związanych z czytaniem i pisaniem i doceniam to, w jaki sposób Event Sourcing ułatwia projektowanie zmian stanu w bazach danych „Read Model”, które różnią się od Twojego Event Store.

Nie jestem do końca pewien, dlaczego kiedykolwiek nawadniałeś swoje Agregaty z samego Sklepu Eventowego.

Jeśli prognozowanie zmian w bazach danych „do odczytu” jest tak łatwe, dlaczego nie zawsze projektować zmiany w bazie danych „do zapisu”, których schemat idealnie pasuje do modelu domeny? Byłaby to skutecznie baza danych migawek.

Wyobrażam sobie, że musi to być dość powszechne w aplikacjach ES + CQRS na wolności.

W takim przypadku, czy Event Store jest przydatny tylko podczas przebudowy bazy danych „write” w wyniku zmian schematu? A może brakuje mi czegoś większego?

MetaFight
źródło
Nie ma nic złego w asynchronicznym zapisie do magazynu stanów modelu zapisu i używaniu go wyłącznie do ładowania jednostek. Te same dokładne problemy dotyczące spójności występują niezależnie od tego, czy to robisz, czy nie. Kluczem do rozwiązania tych problemów ze spójnością jest odmienne modelowanie jednostek. W pozyskiwaniu zdarzeń nie ma nic magicznego, co rozwiązałoby te problemy ze spójnością. Magia polega na modelowaniu, a nie opiece. Istnieją określone aplikacje wymagające spójności na tym poziomie, które mają wysoce kontrowersyjne byty, bez względu na to, jak je modelujesz i wymagają specjalnej uwagi niezależnie.
Andrew Larsson,
Tak długo, jak możesz zagwarantować dostawę wydarzeń. Aby to zrobić, aplikacja musi po prostu synchronicznie opublikować zdarzenie na trwałej magistrali zdarzeń. Po opublikowaniu praca aplikacji jest zakończona. Magistrala dostarczy go następnie do różnych programów obsługi zdarzeń: jeden do aktualizacji magazynu zdarzeń, jeden do aktualizacji magazynu stanu i wszelkie inne potrzebne do aktualizacji magazynów odczytu. Powodem, dla którego korzystasz z Event Sourcing, jest to, że nie dbasz już o natychmiastową spójność. Przyjmij to.
Andrew Larsson,
Nie ma powodu, abyś ciągle ładował swoje byty ze sklepu z wydarzeniami. To nie jest jego cel. Jego celem jest zapewnienie nieprzetworzonej, stałej księgi wszystkiego, co wydarzyło się w systemie. Magazyny stanów encji i denormalizowane modele odczytu służą do ładowania i odczytu.
Andrew Larsson,

Odpowiedzi:

14

Nie jestem do końca pewien, dlaczego kiedykolwiek nawadniałeś swoje Agregaty z samego Sklepu Eventowego.

Ponieważ „wydarzenia” to księgi rekordów.

Jeśli prognozowanie zmian w bazach danych „do odczytu” jest tak łatwe, dlaczego nie zawsze projektować zmiany w bazie danych „do zapisu”, których schemat idealnie pasuje do modelu domeny? Byłaby to skutecznie baza danych migawek.

Tak; czasem użyteczną optymalizacją wydajności jest użycie buforowanej kopii stanu agregacji, a nie generowanie tego stanu od nowa za każdym razem. Pamiętaj: pierwszą zasadą optymalizacji wydajności jest „nie”. Zwiększa to złożoność rozwiązania i wolisz go unikać, dopóki nie uzyskasz przekonującej motywacji biznesowej.

W takim przypadku, czy Event Store jest przydatny tylko podczas przebudowy bazy danych „write” w wyniku zmian schematu? A może brakuje mi czegoś większego?

Brakuje czegoś większego.

Po pierwsze, jeśli zastanawiasz się nad rozwiązaniem pochodzącym od zdarzenia, dzieje się tak dlatego, że oczekujesz wartości w zachowaniu historii tego, co się wydarzyło, to znaczy, że chcesz wprowadzać nieniszczące zmiany .

Dlatego w ogóle piszemy do sklepu z wydarzeniami.

W szczególności oznacza to, że każdą zmianę należy zapisać w magazynie zdarzeń.

Rywalizujący pisarze mogą potencjalnie albo zniszczyć nawzajem swoje zapisy, albo doprowadzić system do niezamierzonego stanu, jeśli nie znają się nawzajem. Tak więc zwykłym podejściem, gdy potrzebujesz spójności, jest skierowanie swoich zapisów do określonej pozycji w czasopiśmie (analogicznie do warunkowego PUT w interfejsie HTTP). Niepowodzenie zapisu informuje pisarza, że ​​ich bieżące rozumienie dziennika nie jest zsynchronizowane i że powinni się zregenerować.

Powrót do znanej dobrej pozycji, a następnie odtworzenie wszelkich dodatkowych zdarzeń, ponieważ ten punkt jest powszechną strategią odzyskiwania. Ta znana dobra pozycja może być kopią tego, co znajduje się w lokalnej pamięci podręcznej lub reprezentacją w sklepie z migawkami.

Na szczęśliwej ścieżce możesz zachować migawkę agregatu w pamięci; wystarczy skontaktować się z zewnętrznym sklepem, gdy nie ma dostępnej kopii lokalnej.

Co więcej, nie musisz być całkowicie pochłonięty, jeśli masz dostęp do księgi rekordów.

Tak więc zwykłym podejściem ( jeśli używa się repozytorium migawek) jest utrzymywanie go asynchronicznie . W ten sposób, jeśli musisz odzyskać, możesz to zrobić bez ponownego ładowania i odtwarzania całej historii agregatu.

W wielu przypadkach ta złożoność nie jest interesująca, ponieważ drobnoziarniste agregaty o określonym czasie życia zwykle nie gromadzą wystarczającej liczby zdarzeń, aby korzyści przewyższyły koszty utrzymania pamięci podręcznej migawki.

Ale gdy jest to właściwe narzędzie do rozwiązania problemu, wczytanie nieaktualnej reprezentacji agregatu do modelu zapisu, a następnie zaktualizowanie go o dodatkowe zdarzenia, jest całkowicie rozsądnym rozwiązaniem.

VoiceOfUnreason
źródło
To wszystko brzmi rozsądnie. Kiedy jakiś czas temu bawiłem się fałszywą implementacją ES, użyłem EventStore, aby zagwarantować spójność, ale także synchronicznie napisałem coś, co nazywacie „repozytorium migawek”. Oznaczało to, że obecny stan był zawsze gotowy do odczytu bez konieczności odtwarzania zdarzeń. Podejrzewałem, że to nie będzie dobrze skalować, ale ponieważ było to tylko ćwiczenie, nie miałem nic przeciwko.
MetaFight,
6

Ponieważ nie określasz, jaki byłby cel bazy danych „zapisu”, przyjmuję tutaj, że masz na myśli to: rejestrując nową aktualizację w agregacji, zamiast odbudowywać agregację ze sklepu zdarzeń, usuń go z bazy danych „write”, sprawdź poprawność zmiany i wydaj zdarzenie.

Jeśli to masz na myśli, ta strategia stworzy warunek niespójności: jeśli nowa aktualizacja nastąpi, zanim ostatnia będzie miała szansę na przejście do bazy danych „zapisu”, nowa aktualizacja zostanie zweryfikowana pod kątem nieaktualnych danych, potencjalnie powodując w ten sposób zdarzenie „niemożliwe” (tj. „niedozwolone”) i niszcząc stan systemu.

Weźmy na przykład stały przykład rezerwacji miejsc w teatrze. Aby zapobiec podwójnej rezerwacji, musisz upewnić się, że zarezerwowane miejsce nie jest już zajęte - to właśnie nazywasz „sprawdzaniem poprawności”. Aby to zrobić, przechowujesz listę już zarezerwowanych miejsc w bazie danych „write”. Następnie, gdy pojawi się prośba o rezerwację, sprawdzasz, czy żądane miejsce znajduje się na liście, a jeśli nie, wydaje wydarzenie „zarezerwowane”, w przeciwnym razie odpowiada komunikatem o błędzie. Następnie uruchamiasz proces projekcji, w którym słuchasz „zarezerwowanych” zdarzeń i dodajesz zarezerwowane miejsca do listy w bazie danych „zapisu”.

Normalnie system działałby w następujący sposób:

1. Request to book seat #1
2. Check in the "already booked" list: the list is empty.
3. Issue a "booked seat #1" event.
4. Projection process catches the event, adds seat #1 to the "already booked" list.
5. Another request to book seat #1.
6. Check in the list: the list contains seat #1
7. Respond with an error message.

Co jednak się stanie, jeśli żądania pojawią się zbyt szybko, a krok 5 nastąpi przed krokiem 4?

1. Request to book seat #1
2. Check in the "already booked" list: the list is empty.
3. Issue a "booked seat #1" event.
4. Another request to book seat #1.
5. Check in the list: the list is still empty.
6. Issue another "booked seat #1" event.

Teraz masz dwa wydarzenia na rezerwację tego samego miejsca. Stan systemu jest uszkodzony.

Aby temu zapobiec, nigdy nie należy sprawdzać poprawności aktualizacji względem projekcji. Aby sprawdzić poprawność aktualizacji, przebuduj agregację ze sklepu zdarzeń, a następnie sprawdź poprawność aktualizacji względem niej. Następnie wydajesz zdarzenie, ale używasz osłony znacznika czasu, aby upewnić się, że nie wydano żadnych nowych zdarzeń od czasu ostatniego czytania w sklepie. Jeśli to się nie powiedzie, po prostu spróbuj ponownie.

Odbudowywanie agregatów ze sklepu zdarzeń może wiązać się z obniżeniem wydajności. Aby temu zaradzić, możesz przechowywać zbiorcze migawki bezpośrednio w strumieniu zdarzeń, oznaczone tagiem ID zdarzenia, z którego utworzono migawkę. W ten sposób można odbudować agregację, ładując najnowszą migawkę i odtwarzając tylko zdarzenia, które nastąpiły po niej, zamiast zawsze odtwarzać cały strumień zdarzeń od początku.

Fiodor Soikin
źródło
Dziękuję za odpowiedź (i przepraszam, że tak długo trwa odpowiedź). To, co mówisz o sprawdzaniu poprawności względem bazy danych zapisu, niekoniecznie jest prawdą. Jak wspomniałem w innym komentarzu, w przykładowej implementacji ES, z którą bawiłem się, upewniłem się, że zaktualizowałem synchronicznie bazę danych zapisu (i zapisałem identyfikator współbieżności / znacznik czasu). Umożliwiło mi to wykrycie optymistycznych naruszeń współbieżności bez konieczności przygotowywania się z EventStore. Oczywiście, same zapisy synchroniczne nie chronią przed uszkodzeniem danych, ale robiłem też zapisy z jednym dostępem (jednowątkowe).
MetaFight,
Rozwiązałem więc problemy ze spójnością. Chociaż założyłem, że dzieje się to kosztem skalowalności.
MetaFight,
Synchroniczne zapisywanie do bazy danych zapisu nadal niesie ryzyko uszkodzenia: co się stanie, jeśli zapis do magazynu zdarzeń zakończy się powodzeniem, ale zapis do bazy danych zapisu się nie powiedzie?
Fiodor Soikin
1
Jeśli projekcja odczytu się nie powiedzie, po prostu spróbuje ponownie, aż się powiedzie. Jeśli natychmiast się zawiesi, obudzi się i będzie kontynuował od miejsca, w którym się rozbił - innymi słowy, również spróbuj ponownie. Obserwowalny na zewnątrz efekt nie różni się niczym od tego, że działa trochę wolniej. Jeśli projekcja ciągle zawiedzie i konsekwentnie zawiedzie, oznaczałoby to, że jest w niej błąd i będzie musiał zostać naprawiony. Po naprawieniu wznowi działanie od ostatniego dobrego stanu. Jeśli cała odczytana baza danych ulegnie uszkodzeniu w wyniku błędu, po prostu odbuduję bazę od zera przy użyciu historii zdarzeń.
Fiodor Soikin
1
Nigdy nie tracisz danych, to jest najważniejszy punkt. Dane mogą na chwilę utknąć w niewygodnym (do odczytu) kształcie, ale nigdy nie zostaną utracone.
Fiodor Soikin,
3

Głównym powodem jest wydajność. Możesz przechowywać migawkę dla każdego zatwierdzenia (zatwierdzenie = zdarzenia generowane przez pojedyncze polecenie, zwykle tylko jedno zdarzenie), ale jest to kosztowne. Wzdłuż migawki musisz przechowywać także zatwierdzenie, w przeciwnym razie nie byłoby to pozyskiwanie zdarzeń. A wszystko to musi odbywać się atomowo, wszystko albo nic. Twoje pytanie jest ważne tylko wtedy, gdy używane są osobne bazy danych / tabele / kolekcje (w przeciwnym razie byłoby to dokładnie pozyskiwanie zdarzeń), więc jesteś zmuszony używać transakcji w celu zagwarantowania spójności. Transakcje nie są skalowalne. Strumień zdarzeń tylko do dołączania (magazyn zdarzeń) jest matką skalowalności.

Drugim powodem jest enkapsulacja zagregowana. Musisz go chronić. Oznacza to, że agregat powinien mieć swobodę zmiany wewnętrznej reprezentacji w dowolnym momencie. Jeśli go przechowujesz i mocno na nim polegasz, będziesz miał trudności z wersjonowaniem, co na pewno się wydarzy. W sytuacji, gdy używasz migawki tylko do optymalizacji, kiedy zmiany schematu po prostu ignorujesz te migawki ( po prostu ? Nie wydaje mi się, że tak; powodzenia w ustaleniu, że schemat agregacji zmienia się - w tym wszystkie zagnieżdżone elementy i obiekty wartości - w efektywny sposób i zarządzanie tym).

Constantin Galbenu
źródło
Czy po zmianie schematu agregacji nie byłoby łatwo odtworzyć moje zdarzenia w celu wygenerowania zaktualizowanej bazy danych „zapisu”?
MetaFight,
Problemem jest wykrycie tej zmiany. Agregat może być bardzo duży, z wieloma plikami / klasami.
Constantin Galbenu,
Nie rozumiem. Zmiana nastąpiłaby w wersji oprogramowania. Wydanie prawdopodobnie byłoby dostarczane ze skryptem bazy danych w celu ponownego wygenerowania bazy danych „zapisu”.
MetaFight,
Skrypt migracji wymaga dużo pracy. Podczas działania aplikacja musi być wyłączona.
Constantin Galbenu,
@MetaFight, jeśli strumień jest bardzo duży, odbudowanie nowego schematu agregacji zajmie dużo czasu ... Myślę teraz o migawce, która jest stanem projekcji na żywo, która mogłaby być uruchomiona przed wydaniem nowej agregacji schemat
Narvalex