Reidratazione degli aggregati da una proiezione di "istantanee" anziché dall'archivio eventi


14

Quindi sto flirtando con Event Sourcing e CQRS da un po 'di tempo, anche se non ho mai avuto l'opportunità di applicare gli schemi su un vero progetto.

Comprendo i vantaggi della separazione delle preoccupazioni di lettura e scrittura e apprezzo il modo in cui Event Sourcing semplifica la proiezione delle modifiche di stato ai database "Leggi modello" che sono diversi dal tuo Archivio eventi.

Ciò su cui non sono molto chiaro è perché dovresti mai reidratare i tuoi aggregati dallo stesso Event Store.

Se proiettare le modifiche ai database "read" è così semplice, perché non proiettare sempre le modifiche su un database "write" il cui schema corrisponde perfettamente al modello di dominio? Questo sarebbe effettivamente un database di istantanee.

Immagino che questo debba essere abbastanza comune nelle applicazioni ES + CQRS in natura.

In tal caso, l'Event Store è utile solo quando si ricostruisce il database "write" a seguito di modifiche allo schema? O mi sto perdendo qualcosa di più grande?


Non c'è nulla di sbagliato nel scrivere in modo asincrono in un archivio di stato del modello di scrittura e utilizzarlo esclusivamente per caricare entità. Sono presenti gli stessi problemi di coerenza esatta, indipendentemente dal fatto che tu lo faccia o meno. La chiave per risolvere questi problemi di coerenza è modellare le entità in modo diverso. Non c'è nulla di magico nel reperimento di eventi che risolva questi problemi di coerenza. La magia sta nella modellazione e non nella cura. Ci sono applicazioni specifiche che richiedono coerenza a quel livello che hanno entità altamente controverse, non importa come le modellate e richiederanno un'attenzione speciale a prescindere.
Andrew Larsson,

Finché puoi garantire la consegna degli eventi. Per fare ciò, l'applicazione deve semplicemente pubblicare un evento in modo sincrono su un bus eventi durevole. Dopo la pubblicazione, il lavoro dell'applicazione è completo. Il bus lo consegnerà quindi a vari gestori di eventi: uno per aggiornare l'archivio eventi, uno per aggiornare l'archivio stato e tutti gli altri necessari per aggiornare gli archivi di lettura. Il motivo per cui stai utilizzando Event Sourcing è perché non ti preoccupi più della coerenza immediata. Abbraccialo.
Andrew Larsson,

Non vi è alcun motivo per cui si dovrebbe caricare costantemente le entità dall'archivio eventi. Non è questo il suo scopo. Il suo scopo è quello di fornire un libro mastro e permanente di tutto ciò che è accaduto nel sistema. Gli archivi dello stato delle entità e i modelli di lettura denormalizzati servono per il caricamento e la lettura.
Andrew Larsson,

Risposte:


14

Ciò su cui non sono molto chiaro è perché dovresti mai reidratare i tuoi aggregati dallo stesso Event Store.

Perché gli "eventi" sono il libro dei record.

Se proiettare le modifiche ai database "read" è così semplice, perché non proiettare sempre le modifiche su un database "write" il cui schema corrisponde perfettamente al modello di dominio? Questo sarebbe effettivamente un database di istantanee.

Sì; a volte è un'utile ottimizzazione delle prestazioni utilizzare una copia cache dello stato aggregato, anziché rigenerare ogni volta tale stato da zero. Ricorda: la prima regola di ottimizzazione delle prestazioni è "No". Aggiunge ulteriore complessità alla soluzione e preferiresti evitarlo fino a quando non avrai una motivazione aziendale convincente.

In tal caso, l'Event Store è utile solo quando si ricostruisce il database "write" a seguito di modifiche allo schema? O mi sto perdendo qualcosa di più grande?

Ti stai perdendo qualcosa di più grande.

Il primo punto è che se stai considerando una soluzione basata sugli eventi, è perché ti aspetti che ci sia un valore nel preservare la storia di ciò che è accaduto, vale a dire che vuoi fare cambiamenti non distruttivi .

Quindi è per questo che stiamo scrivendo al negozio di eventi.

In particolare, ciò significa che ogni modifica deve essere scritta nell'archivio eventi.

Scrittori in competizione potrebbero potenzialmente distruggere le scritture reciproche o portare il sistema in uno stato involontario, se non sono a conoscenza delle modifiche reciproche. Quindi il solito approccio quando hai bisogno di coerenza è indirizzare le tue scritture in una posizione specifica nel journal (analogo a un PUT condizionale in un'API HTTP). Una scrittura fallita dice allo scrittore che la loro attuale comprensione del giornale non è sincronizzata e che dovrebbero recuperare.

Ritornare a una buona posizione nota quindi riprodurre eventuali eventi aggiuntivi poiché quel punto è una strategia di recupero comune. Quella buona posizione conosciuta può essere una copia di ciò che è nella cache locale o una rappresentazione nel tuo archivio di istantanee.

Nel percorso felice, è possibile mantenere un'istantanea dell'aggregato in memoria; devi solo contattare un negozio esterno quando non è disponibile una copia locale.

Inoltre, non devi essere completamente coinvolto, se hai accesso al libro dei record.

Quindi il solito approccio ( se si utilizza un repository di snapshot) è mantenerlo in modo asincrono . In questo modo, se è necessario ripristinare, è possibile farlo senza ricaricare e riprodurre l'intera cronologia dell'aggregato.

Esistono molti casi in cui questa complessità non è interessante, poiché gli aggregati a grana fine con tempi di vita mirati di solito non raccolgono eventi sufficienti per i benefici da superare i costi di gestione di una cache di snapshot.

Ma quando è lo strumento giusto per il problema, caricare una rappresentazione non aggiornata dell'aggregato nel modello di scrittura, quindi aggiornarlo con eventi aggiuntivi, è una cosa perfettamente ragionevole da fare.


Sembra tutto ragionevole. Quando ho giocato con un'implementazione fittizia di ES qualche tempo fa, ho usato EventStore per garantire coerenza, ma ho anche scritto in modo sincrono in quello che tu chiami un "repository di istantanee". Ciò significava che lo stato corrente era sempre pronto per la lettura senza dover ripetere gli eventi. Sospettavo che non si sarebbe adattato bene, ma dato che era solo un esercizio non mi dispiaceva.
MetaFight,

6

Dato che non specifichi quale sarebbe lo scopo del database "write", assumerò qui che intendi questo: quando registri un nuovo aggiornamento in un aggregato, invece di ricostruire l'aggregato dall'archivio eventi, tu estrarlo dal database "write", convalidare la modifica ed emettere un evento.

Se questo è ciò che intendi, allora questa strategia creerà una condizione per l'incoerenza: se un nuovo aggiornamento si verifica prima che l'ultimo abbia avuto la possibilità di inserirlo nel database "write", il nuovo aggiornamento verrà convalidato rispetto a dati obsoleti, quindi potenzialmente emettendo un evento "impossibile" (cioè "non consentito") e corrompendo lo stato del sistema.

Ad esempio, considera un esempio permanente di prenotazione di posti in un teatro. Per evitare la doppia prenotazione, è necessario assicurarsi che il posto prenotato non sia già occupato: questo è ciò che si chiama "convalida". Per fare ciò, memorizzi un elenco di posti già prenotati nel database "write". Quindi, quando arriva una richiesta di prenotazione, controlli se il posto richiesto è nell'elenco e, in caso contrario, emetti un evento "prenotato", altrimenti rispondi con un messaggio di errore. Quindi si esegue un processo di proiezione, in cui si ascoltano gli eventi "prenotati" e si aggiungono i posti prenotati all'elenco nel database "write".

Normalmente, il sistema funzionerebbe in questo modo:

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.

Tuttavia, cosa succede se le richieste arrivano troppo rapidamente e il passaggio 5 si verifica prima del passaggio 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.

Ora hai due eventi per prenotare lo stesso posto. Lo stato del sistema è danneggiato.

Per evitare che ciò accada, non è mai necessario convalidare gli aggiornamenti rispetto a una proiezione. Per convalidare un aggiornamento, è necessario ricostruire l'aggregato dall'archivio eventi, quindi convalidare l'aggiornamento rispetto ad esso. Successivamente, si emette un evento, ma si utilizza la protezione data / ora per assicurarsi che non siano stati emessi nuovi eventi dall'ultima lettura dal negozio. Se fallisce, riprova.

La ricostruzione di aggregati dal negozio di eventi potrebbe comportare una penalità per le prestazioni. Per mitigarlo, puoi archiviare istantanee aggregate direttamente nel flusso di eventi, etichettate con ID dell'evento da cui è stata creata l'istantanea. In questo modo, è possibile ricostruire l'aggregato caricando lo snapshot più recente e riproducendo solo gli eventi che ne sono seguiti, invece di riprodurre sempre l'intero flusso di eventi dall'inizio dei tempi.


Grazie per la tua risposta (e scusa per aver impiegato così tanto tempo a rispondere). Quello che dici sulla convalida contro il database di scrittura non è necessariamente vero. Come ho già detto in un altro commento, in un esempio di implementazione ES con cui stavo giocando, mi sono assicurato di aggiornare il mio database di scrittura in modo sincrono (e archiviare il concurrencyId / timestamp). Ciò mi ha permesso di rilevare violazioni ottimistiche della concorrenza senza che sia necessario prepararsi dall'EventStore. Certo, le scritture sincrone da sole non proteggono dalla corruzione dei dati, ma stavo anche scrivendo ad accesso singolo (single thread).
MetaFight,

Quindi, ho risolto i problemi di coerenza. Tuttavia, ho ipotizzato che ciò fosse a scapito della scalabilità.
MetaFight,

La scrittura sincrona nel database di scrittura comporta ancora un rischio di corruzione: cosa succede se la scrittura nell'archivio eventi ha esito positivo, ma la scrittura nel database di scrittura ha esito negativo?
Fyodor Soikin,

1
Se la proiezione in lettura non riesce, riproverà fino a quando non avrà successo. Se si blocca completamente, si risveglierà e continuerà da dove si è schiantato - in altre parole, anche riprovare. L'effetto osservabile esterno non sarebbe diverso da quello che corre solo un po 'lento. Se la proiezione continua a fallire e fallire costantemente, ciò significherebbe che c'è un bug e che dovrà essere risolto. Dopo la correzione, riprenderà a funzionare dall'ultimo stato buono. Se l'intero database letto viene danneggiato a causa del bug, ricostruirò il database da zero utilizzando la cronologia degli eventi.
Fyodor Soikin,

1
Nessun dato viene mai perso, questo è il punto cruciale. I dati possono essere bloccati in una forma scomoda (per la lettura) per un po ', ma non vengono mai persi.
Fyodor Soikin,

3

Il motivo principale è la prestazione. È possibile memorizzare un'istantanea per ogni commit (commit = gli eventi generati da un singolo comando, di solito solo un evento) ma questo è costoso. Lungo l'istantanea è necessario memorizzare anche il commit, altrimenti non si tratterebbe di un approvvigionamento di eventi. E tutto ciò deve essere fatto atomicamente, tutto o niente. La tua domanda è valida solo se vengono utilizzati database / tabelle / raccolte separate (altrimenti sarebbe esattamente un approvvigionamento di eventi), quindi sei costretto a utilizzare le transazioni al fine di garantire coerenza. Le transazioni non sono scalabili. Un flusso di eventi solo append (l'archivio eventi) è la madre della scalabilità.

Il secondo motivo è l'incapsulamento aggregato. Devi proteggerlo. Ciò significa che l'aggregato dovrebbe essere libero di modificare la propria rappresentazione interna in qualsiasi momento. Se lo memorizzi e dipendi fortemente da esso, avrai un momento molto difficile con il controllo delle versioni, il che accadrà di sicuro. Nella situazione in cui usi l'istantanea solo come ottimizzazione, quando cambi lo schema semplicemente ignori quelle istantanee ( semplicemente ? Io non la penso davvero; buona fortuna nel determinare che le modifiche dello schema di Aggregate - incluse tutte le entità nidificate e gli oggetti valore - in un modo efficiente e gestirlo).


Quando il mio schema di aggregazione cambia, non sarebbe semplice riprodurre i miei eventi per generare un database di "scrittura" aggiornato?
MetaFight,

Il problema è rilevare quel cambiamento. Un aggregato potrebbe essere molto grande, con molti file / classi.
Constantin Galbenu,

Non capisco. La modifica avverrebbe con una versione del software. Il rilascio probabilmente verrebbe con uno script del database per rigenerare il database "write".
MetaFight,

C'è molto lavoro da fare per uno script di migrazione. Mentre è in esecuzione l'app deve essere inattiva.
Constantin Galbenu,

@MetaFight se il flusso è molto grande, ci vorrà molto tempo per ricostruire il nuovo schema aggregato ... Sto pensando ora a un'istantanea che è uno stato di una proiezione live che potrebbe essere in esecuzione prima del rilascio del nuovo aggregato schema
Narvalex,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.