Używanie RDBMS jako magazynu do pozyskiwania zdarzeń

119

Gdybym używał RDBMS (np. SQL Server) do przechowywania danych dotyczących źródeł zdarzeń, jak mógłby wyglądać schemat?

Widziałem kilka odmian, o których mówiono w abstrakcyjnym sensie, ale nic konkretnego.

Na przykład załóżmy, że istnieje jednostka „Produkt”, a zmiany w tym produkcie mogą przybrać postać: ceny, kosztu i opisu. Nie wiem, czy:

  1. Miej tabelę „ProductEvent”, która zawiera wszystkie pola produktu, gdzie każda zmiana oznacza nowy rekord w tej tabeli, plus „kto, co, gdzie, dlaczego, kiedy i jak” (WWWWWH). W przypadku zmiany kosztu, ceny lub opisu dodawany jest cały nowy wiersz reprezentujący Produkt.
  2. Przechowuj Koszt, Cena i Opis produktu w oddzielnych tabelach połączonych z tabelą Produkt za pomocą relacji klucza obcego. Gdy wystąpią zmiany tych właściwości, napisz nowe wiersze z WWWWWH, jeśli to konieczne.
  3. Przechowuj WWWWWH oraz serializowany obiekt reprezentujący zdarzenie w tabeli „ProductEvent”, co oznacza, że ​​samo zdarzenie musi zostać załadowane, zdeserializowane i ponownie odtworzone w kodzie mojej aplikacji w celu odtworzenia stanu aplikacji dla danego Produktu .

Szczególnie martwi mnie opcja 2 powyżej. W skrajnym przypadku tabela produktów byłaby prawie jedną tabelą na właściwość, gdzie załadowanie stanu aplikacji dla danego produktu wymagałoby załadowania wszystkich zdarzeń dla tego produktu z każdej tabeli zdarzeń produktu. Ta eksplozja stołu źle mi pachnie.

Jestem pewien "to zależy" i chociaż nie ma jednej "poprawnej odpowiedzi", staram się wyczuć, co jest akceptowalne, a co całkowicie nie do przyjęcia. Zdaję sobie również sprawę, że NoSQL może tu pomóc, gdzie zdarzenia mogą być przechowywane w zagregowanym katalogu głównym, co oznacza tylko jedno żądanie do bazy danych, aby uzyskać zdarzenia w celu odbudowania obiektu, ale nie używamy bazy danych NoSQL w chwilę, więc szukam alternatyw.

Neil Barnwell
źródło
2
W najprostszej postaci: [Event] {AggregateId, AggregateVersion, EventPayload}. Nie ma potrzeby podawania typu agregatu, ale MOŻESZ opcjonalnie go przechowywać. Nie ma potrzeby podawania typu zdarzenia, ale MOŻESZ opcjonalnie go zapisać. To długa lista rzeczy, które się wydarzyły, wszystko inne to tylko optymalizacja.
Yves Reynhout
7
Zdecydowanie trzymaj się z dala od punktów 1 i 2. Serializuj wszystko do postaci blob i przechowuj w ten sposób.
Jonathan Oliver

Odpowiedzi:

109

Magazyn zdarzeń nie powinien potrzebować informacji o konkretnych polach lub właściwościach zdarzeń. W przeciwnym razie każda modyfikacja modelu spowodowałaby konieczność migracji bazy danych (podobnie jak w przypadku dobrej, staromodnej trwałości opartej na stanie). Dlatego w ogóle nie polecałbym opcji 1 i 2.

Poniżej znajduje się schemat używany w Ncqrs . Jak widać, tabela „Zdarzenia” przechowuje powiązane dane jako CLOB (tj. JSON lub XML). Odpowiada to Twojej opcji 3 (tylko, że nie ma tabeli „ProductEvents”, ponieważ potrzebujesz tylko jednej ogólnej tabeli „Events”. W Ncqrs mapowanie na Twoje Aggregate Roots odbywa się za pośrednictwem tabeli „EventSources”, gdzie każde EventSource odpowiada rzeczywistej Agregat główny).

Table Events:
    Id [uniqueidentifier] NOT NULL,
    TimeStamp [datetime] NOT NULL,

    Name [varchar](max) NOT NULL,
    Version [varchar](max) NOT NULL,

    EventSourceId [uniqueidentifier] NOT NULL,
    Sequence [bigint], 

    Data [nvarchar](max) NOT NULL

Table EventSources:
    Id [uniqueidentifier] NOT NULL, 
    Type [nvarchar](255) NOT NULL, 
    Version [int] NOT NULL

Mechanizm trwałości SQL implementacji magazynu zdarzeń Jonathana Olivera składa się w zasadzie z jednej tabeli o nazwie „Commits” z polem BLOB „Payload”. Jest to prawie to samo, co w Ncqrs, tyle że serializuje właściwości zdarzenia w formacie binarnym (który na przykład dodaje obsługę szyfrowania).

Greg Young zaleca podobne podejście, co zostało obszernie udokumentowane na stronie internetowej Grega .

Schemat jego prototypowej tabeli „Zdarzenia” brzmi:

Table Events
    AggregateId [Guid],
    Data [Blob],
    SequenceNumber [Long],
    Version [Int]
Dennis Traub
źródło
9
Niezła odpowiedź! Jednym z głównych argumentów, o których wciąż czytam, aby korzystać z EventSourcing, jest możliwość przeszukiwania historii. Jak mam zrobić narzędzie raportowania, które będzie wydajne w wykonywaniu zapytań, gdy wszystkie interesujące dane są serializowane jako XML lub JSON? Czy są jakieś interesujące artykuły dotyczące rozwiązania opartego na tabeli?
Marijn Huizendveld
11
@MarijnHuizendveld prawdopodobnie nie chcesz wykonywać zapytań dotyczących samego magazynu zdarzeń. Najczęstszym rozwiązaniem byłoby podłączenie kilku programów obsługi zdarzeń, które projektują zdarzenia do bazy danych raportowania lub BI. Powtórzenie historii zdarzeń względem tych programów obsługi.
Dennis Traub
1
@Denis Traub dzięki za odpowiedź. Dlaczego nie zapytać o sam magazyn zdarzeń? Obawiam się, że stanie się to dość nieporządne / intensywne, jeśli będziemy musieli powtarzać całą historię za każdym razem, gdy wymyślimy nową sprawę BI?
Marijn Huizendveld
1
Pomyślałem, że w pewnym momencie powinieneś mieć również tabele oprócz magazynu zdarzeń, aby przechowywać dane z modelu w jego najnowszym stanie? I że podzielisz model na model do odczytu i model do zapisu. Model zapisu jest sprzeczny z magazynem zdarzeń, a magazyn zdarzeń aktualizuje do modelu odczytu. Model do odczytu zawiera tabele, które reprezentują jednostki w systemie, dzięki czemu można używać modelu odczytu do raportowania i przeglądania. Musiałem coś źle zrozumieć.
theBoringCoder
10
@theBoringCoder Wygląda na to, że pozyskiwanie zdarzeń i CQRS są zdezorientowane lub przynajmniej zgniecione w głowie. Często można je znaleźć razem, ale nie są tym samym. CQRS pozwala oddzielić modele odczytu i zapisu, podczas gdy pozyskiwanie zdarzeń umożliwia użycie strumienia zdarzeń jako pojedynczego źródła prawdy w aplikacji.
Bryan Anderson
7

Projekt GitHub CQRS.NET zawiera kilka konkretnych przykładów tego, jak można zrobić EventStores w kilku różnych technologiach. W chwili pisania tego tekstu istnieje implementacja w języku SQL przy użyciu Linq2SQL i zgodny ze schematem SQL , jeden dla MongoDB , jeden dla DocumentDB (CosmosDB, jeśli jesteś na Azure) i jeden wykorzystujący EventStore (jak wspomniano powyżej). Na platformie Azure jest więcej, takich jak Table Storage i Blob Storage, które są bardzo podobne do płaskiego magazynu plików.

Myślę, że głównym celem jest to, że wszystkie są zgodne z tym samym zleceniem / umową. Wszystkie przechowują informacje w jednym miejscu / kontenerze / tabeli, używają metadanych do identyfikacji jednego zdarzenia od innego i „po prostu” przechowują całe zdarzenie tak, jak było - w niektórych przypadkach serializowane, w technologiach pomocniczych, tak jak było. W zależności od tego, czy wybierzesz bazę danych dokumentów, relacyjną bazę danych lub nawet plik płaski, istnieje kilka różnych sposobów, aby osiągnąć ten sam cel magazynu zdarzeń (jest to przydatne, jeśli w dowolnym momencie zmienisz zdanie i stwierdzisz, że musisz przeprowadzić migrację lub wesprzeć więcej niż jednej technologii przechowywania).

Jako programista przy projekcie mogę podzielić się spostrzeżeniami na temat niektórych wyborów, których dokonaliśmy.

Po pierwsze stwierdziliśmy (nawet z unikalnymi identyfikatorami UUID / GUID zamiast liczb całkowitych) z wielu powodów identyfikatory sekwencyjne występują ze względów strategicznych, dlatego samo posiadanie identyfikatora nie było wystarczająco unikalne dla klucza, więc połączyliśmy naszą główną kolumnę klucza identyfikatora z danymi / typ obiektu, aby stworzyć coś, co powinno być naprawdę (w sensie aplikacji) unikalnym kluczem. Wiem, że niektórzy ludzie mówią, że nie musisz go przechowywać, ale będzie to zależeć od tego, czy jesteś od podstaw, czy też musisz współistnieć z istniejącymi systemami.

Utknęliśmy z jednym kontenerem / tabelą / kolekcją ze względu na łatwość konserwacji, ale bawiliśmy się z oddzielną tabelą na jednostkę / obiekt. W praktyce stwierdziliśmy, że oznacza to, że aplikacja potrzebuje uprawnień „TWÓRZ” (co ogólnie nie jest dobrym pomysłem ... generalnie zawsze są wyjątki / wykluczenia) lub za każdym razem, gdy nowy obiekt / obiekt powstawał lub był wdrażany, nowy potrzebne do wykonania pojemniki / stoły / kolekcje magazynowe. Okazało się, że było to boleśnie powolne w przypadku lokalnego rozwoju i problematyczne w przypadku wdrożeń produkcyjnych. Możesz nie, ale takie było nasze doświadczenie w świecie rzeczywistym.

Inną rzeczą do zapamiętania jest to, że żądanie wykonania akcji X może spowodować wystąpienie wielu różnych zdarzeń, a zatem poznanie wszystkich zdarzeń wygenerowanych przez polecenie / zdarzenie / cokolwiek jest przydatne. Mogą również dotyczyć różnych typów obiektów, np. Naciśnięcie przycisku „kup” w koszyku może wywołać uruchomienie zdarzeń dotyczących konta i magazynu. Konsumująca aplikacja może chcieć wiedzieć to wszystko, dlatego dodaliśmy CorrelationId. Oznaczało to, że konsument mógł poprosić o wszystkie zdarzenia wywołane w wyniku jego żądania. Zobaczysz to w schemacie .

W szczególności w przypadku SQL odkryliśmy, że wydajność naprawdę stała się wąskim gardłem, jeśli indeksy i partycje nie były odpowiednio używane. Pamiętaj, że jeśli używasz migawek, wydarzenia będą musiały być przesyłane strumieniowo w odwrotnej kolejności. Wypróbowaliśmy kilka różnych indeksów i stwierdziliśmy, że w praktyce do debugowania rzeczywistych aplikacji w środowisku produkcyjnym potrzebne są pewne dodatkowe indeksy. Ponownie zobaczysz to w schemacie .

Inne metadane w trakcie produkcji były przydatne podczas dochodzeń opartych na produkcji, a sygnatury czasowe dały nam wgląd w kolejność utrwalania i zgłaszania zdarzeń. To dało nam pewną pomoc przy szczególnie silnie sterowanym zdarzeniami systemie, który generował ogromną liczbę zdarzeń, dając nam informacje o wydajności takich rzeczy, jak sieci i dystrybucja systemów w sieci.

cdmdotnet
źródło
To jest świetne, dziękuję. Tak się składa, że ​​już dawno temu napisałem to pytanie, kilka samodzielnie zbudowałem w ramach mojej biblioteki Inforigami.Regalo na githubie. Implementacje RavenDB, SQL Server i EventStore. Zastanawiałem się nad zrobieniem tego opartego na plikach, dla śmiechu. :)
Neil Barnwell
1
Twoje zdrowie. Dodałem odpowiedź głównie dla innych, którzy zetknęli się z nią w ostatnim czasie i podzielili się niektórymi z wyciągniętych wniosków, a nie tylko wynikiem.
cdmdotnet
3

Cóż, możesz rzucić okiem na Datomic.

Datomic to baza elastycznych, opartych na czasie faktów , obsługująca zapytania i łączenia, z elastyczną skalowalnością i transakcjami ACID.

Tutaj napisałem szczegółową odpowiedź

Możesz obejrzeć wykład Stuarta Hallowaya wyjaśniający konstrukcję Datomic tutaj

Ponieważ Datomic przechowuje fakty w czasie, możesz go używać do pozyskiwania informacji o zdarzeniach i nie tylko.

kisai
źródło
2

Myślę, że rozwiązanie (1 i 2) może bardzo szybko stać się problemem w miarę rozwoju modelu domeny. Tworzone są nowe pola, niektóre zmieniają znaczenie, a niektóre mogą przestać być używane. Ostatecznie twoja tabela będzie miała dziesiątki pól dopuszczalnych wartości null, a ładowanie zdarzeń będzie bałagan.

Pamiętaj również, że magazyn zdarzeń powinien być używany tylko do zapisu, a wysyłasz do niego zapytania tylko w celu załadowania zdarzeń, a nie właściwości agregatu. Są to odrębne rzeczy (to jest istota CQRS).

Rozwiązanie 3 To, co ludzie zwykle robią, jest na to wiele sposobów.

Na przykład EventFlow CQRS używany z SQL Server tworzy tabelę o następującym schemacie:

CREATE TABLE [dbo].[EventFlow](
    [GlobalSequenceNumber] [bigint] IDENTITY(1,1) NOT NULL,
    [BatchId] [uniqueidentifier] NOT NULL,
    [AggregateId] [nvarchar](255) NOT NULL,
    [AggregateName] [nvarchar](255) NOT NULL,
    [Data] [nvarchar](max) NOT NULL,
    [Metadata] [nvarchar](max) NOT NULL,
    [AggregateSequenceNumber] [int] NOT NULL,
 CONSTRAINT [PK_EventFlow] PRIMARY KEY CLUSTERED 
(
    [GlobalSequenceNumber] ASC
)

gdzie:

  • GlobalSequenceNumber : Prosta globalna identyfikacja, może służyć do porządkowania lub identyfikowania brakujących zdarzeń podczas tworzenia projekcji (readmodel).
  • BatchId : Identyfikacja grupy zdarzeń, które zostały wstawione atomowo (TBH, nie mają pojęcia, dlaczego byłoby to przydatne)
  • AggregateId : Identyfikacja agregatu
  • Dane : zdarzenie serializowane
  • Metadane : inne przydatne informacje ze zdarzenia (np. Typ zdarzenia używany do deserializacji, znacznik czasu, identyfikator inicjatora z polecenia itp.)
  • AggregateSequenceNumber : numer sekwencji w ramach tego samego agregatu (jest to przydatne, jeśli nie możesz zapisywać w kolejności, więc możesz użyć tego pola, aby uzyskać optymistyczną współbieżność)

Jeśli jednak tworzysz od zera, zalecałbym przestrzeganie zasady YAGNI i tworzenie z minimalną wymaganą liczbą pól dla twojego przypadku użycia.

Fabio Marreco
źródło
Twierdziłbym, że BatchId może potencjalnie być powiązany z CorrelationId i CausationId. Służy do ustalania przyczyn zdarzeń i łączenia ich w razie potrzeby.
Daniel Park
Mogłoby być. Jakkolwiek tak jest, sensowne byłoby zapewnienie sposobu na dostosowanie go (np. Ustawienie jako id żądania), ale framework tego nie robi.
Fabio Marreco
1

Możliwa wskazówka to projekt, po którym następuje „Wolno zmieniający się wymiar” (typ = 2), który powinien pomóc w uwzględnieniu:

  • kolejność zachodzących zdarzeń (za pomocą klucza zastępczego)
  • trwałość każdego stanu (ważny od - ważny do)

Zaimplementowanie funkcji składania w lewo również powinno być w porządku, ale trzeba pomyśleć o przyszłej złożoności zapytań.

Viktor Nakonechnyy
źródło
1

Myślę, że byłaby to późna odpowiedź, ale chciałbym zwrócić uwagę, że użycie RDBMS jako magazynu do pozyskiwania zdarzeń jest całkowicie możliwe, jeśli wymagania dotyczące przepustowości nie są wysokie. Chciałbym tylko pokazać przykłady księgi źródłowej zdarzeń, którą zbudowałem, aby zilustrować.

https://github.com/andrewkkchan/client-ledger-service Powyżej znajduje się usługa internetowa księgi źródeł zdarzeń. https://github.com/andrewkkchan/client-ledger-core-db A powyższe używam RDBMS do obliczania stanów, więc możesz cieszyć się wszystkimi zaletami oferowanymi przez RDBMS, takie jak obsługa transakcji. https://github.com/andrewkkchan/client-ledger-core-memory I mam innego konsumenta, który przetwarza w pamięci, aby obsłużyć serie.

Można by argumentować, że rzeczywisty magazyn zdarzeń powyżej nadal istnieje w Kafce - ponieważ RDBMS jest powolny do wstawiania, zwłaszcza gdy wstawianie jest zawsze dołączane.

Mam nadzieję, że kod pomoże ci zilustrować, oprócz bardzo dobrych teoretycznych odpowiedzi już udzielonych na to pytanie.

Andrew Chan
źródło
Dzięki. Już dawno zbudowałem implementację opartą na SQL. Nie jestem pewien, dlaczego RDBMS działa wolno w przypadku wstawiania, chyba że gdzieś dokonałeś nieefektywnego wyboru klucza klastrowego. Tylko dołączanie powinno wystarczyć.
Neil Barnwell