Gestione della concorrenza ES / CQRS


20

Di recente ho iniziato a immergermi in CQRS / ES perché potrebbe essere necessario applicarlo sul posto di lavoro. Sembra molto promettente nel nostro caso, poiché risolverebbe molti problemi.

Ho delineato la mia comprensione approssimativa di come un'app ES / CQRS dovrebbe apparire contestualizzata a un caso d'uso bancario semplificato (prelievo di denaro).

ES / CQRS

Riassumendo, se la persona A ritira del denaro:

  • viene emesso un comando
  • il comando viene consegnato per la convalida / verifica
  • un evento viene inviato a un archivio eventi se la convalida ha esito positivo
  • un aggregatore dequeues l'evento per applicare modifiche sull'aggregato

Da quello che ho capito, il registro degli eventi è la fonte della verità, in quanto è il registro di FATTI, possiamo quindi ricavarne qualsiasi proiezione.


Ora, ciò che non capisco, in questo grande schema di cose, è ciò che accade in questo caso:

  • regola: un saldo non può essere negativo
  • la persona A ha un saldo di 100e
  • la persona A emette un comando di prelievo di 100e
  • la convalida passa e viene emesso l'evento MoneyWithdrewEvent of 100e
  • nel frattempo, la persona A emette un altro comando Prelievo di 100e
  • il primo MoneyWithdrewEvent non è stato aggregato, quindi passa la convalida, poiché il controllo di convalida rispetto all'aggregato (che non è stato ancora aggiornato)
  • MoneyWithdrewEvent of 100e viene emesso un'altra volta

==> Siamo in uno stato incoerente di un saldo a -100e e il registro contiene 2 MoneyWithdrewEvent

A quanto ho capito, ci sono diverse strategie per affrontare questo problema:

  • a) inserire l'ID della versione aggregata insieme all'evento nell'archivio eventi, quindi se si verifica una mancata corrispondenza della versione in caso di modifica, non accade nulla
  • b) utilizzare alcune strategie di blocco, il che implica che il livello di verifica deve in qualche modo crearne uno

Domande relative alle strategie:

  • a) In questo caso, il registro eventi non è più la fonte della verità, come gestirla? Inoltre, siamo tornati al client OK mentre era totalmente sbagliato consentire il prelievo, è meglio in questo caso utilizzare i blocchi?
  • b) Locks == deadlocks, hai qualche idea sulle migliori pratiche?

Nel complesso, la mia comprensione è corretta su come gestire la concorrenza?

Nota: capisco che la stessa persona che preleva due volte i soldi in così poco tempo è impossibile, ma ho preso un semplice esempio, per non perdersi nei dettagli


Perché non aggiornare l'aggregato al passaggio 4 invece di attendere fino al passaggio 7?
Erik Eidt,

Quindi vuoi dire che in questo caso, l'archivio eventi è solo un registro che viene letto solo all'avvio dell'applicazione per ricreare aggregati / altre proiezioni?
Louis F.,

Risposte:


19

Ho delineato la mia comprensione approssimativa di come un'app ES / CQRS dovrebbe apparire contestualizzata a un caso d'uso bancario semplificato (prelievo di denaro).

Questo è l'esempio perfetto di un'applicazione proveniente da eventi. Iniziamo.

Ogni volta che un comando viene elaborato o riprovato (capirai, sii paziente) vengono eseguiti i seguenti passaggi:

  1. il comando raggiunge un gestore comandi, ovvero un servizio in Application layer.
  2. il gestore comandi identifica Aggregatee carica dal repository (in questo caso il caricamento viene eseguito new-ing Aggregateun'istanza, recuperando tutti gli eventi precedentemente emessi di questo aggregato e riapplicandoli all'aggregato stesso; la versione aggregata viene archiviata per uso successivo; dopo l'applicazione degli eventi, l'aggregato si trova nello stato finale, ovvero il saldo del conto corrente viene calcolato come un numero)
  3. il gestore dei comandi chiama il metodo appropriato su Aggregate, like Account::withdrawMoney(100)e raccoglie gli eventi prodotti, ovvero MoneyWithdrewEvent(AccountId, 100); se non ci sono abbastanza soldi nel conto (saldo <100), viene sollevata un'eccezione e tutto viene interrotto; in caso contrario, viene eseguito il passaggio successivo.
  4. il gestore comandi tenta di persistere nel Aggregaterepository (in questo caso il repository è il Event Store); lo fa aggiungendo i nuovi eventi al Event streamse e solo se il versiondel Aggregateè ancora quello che era quando è Aggregatestato caricato. Se la versione non è la stessa, viene ripetuto il comando - andare al passaggio 1 . Se lo versionstesso è uguale, gli eventi vengono aggiunti a Event streame al client viene fornito lo Successstato.

Questo controllo della versione si chiama blocco ottimistico ed è un meccanismo di blocco generale. Un altro meccanismo è il blocco pessimistico quando altri scritti sono bloccati (come in non avviati) fino al completamento di quello corrente.

Il termine Event streamè un'astrazione attorno a tutti gli eventi che sono stati emessi dallo stesso aggregato.

Dovresti capire che Event storeè solo un altro tipo di persistenza in cui sono archiviate tutte le modifiche a un aggregato, non solo lo stato finale.

a) In questo caso, il registro eventi non è più la fonte della verità, come gestirla? Inoltre, siamo tornati al client OK mentre era totalmente sbagliato consentire il prelievo, è meglio in questo caso utilizzare i blocchi?

Il negozio di eventi è sempre la fonte della verità.

b) Locks == deadlocks, hai qualche idea sulle migliori pratiche?

Usando il blocco ottimistico non hai blocchi, basta solo riprovare.

Comunque, Locks! = Deadlock


2
Esistono alcune ottimizzazioni per quanto riguarda il caricamento di un punto in Aggregatecui non si applicano tutti gli eventi ma si mantiene un'istantanea del Aggregatemassimo fino a un punto nel passato e si applicano solo gli eventi accaduti dopo quel punto.
Constantin Galbenu,

Ok, penso che la mia confusione derivi dal fatto che il negozio di eventi == bus degli eventi (ho in mente kafka), quindi ricostruire l'aggregazione potrebbe essere costoso in quanto potrebbe essere necessario rileggere MOLTI eventi. Nel caso di avere un'istantanea di Aggregate, quando è necessario aggiornare l'istantanea? L'archivio snapshot è uguale all'archivio eventi o è una vista materializzata derivata dal bus eventi?
Louis F.

Esistono alcune strategie per la creazione dell'istantanea. Uno è quello di fare un'istantanea ogni n eventi. È necessario memorizzare l'istantanea insieme agli eventi, nello stesso posto / persistenza / database, sullo stesso commit. L'idea è che l'istantanea è fortemente correlata alla versione dell'aggregato.
Constantin Galbenu,

Ok, penso di avere una visione più chiara su come gestirlo. Ora ultima domanda, qual è il ruolo del bus degli eventi alla fine? Se gli aggregati vengono aggiornati in modo sincrono?
Louis F.

1
Sì, è possibile utilizzare un RabbitMQ o qualsiasi canale si desideri inviare gli eventi ai modelli letti in modo asincrono, ma solo dopo averli mantenuti nell'archivio eventi. In effetti, nessuna convalida degli eventi viene eseguita dopo che sono persistiti: gli eventi rappresentano fatti accaduti; un modello letto può o meno piacere che qualcosa sia successo ma non può cambiare la cronologia.
Constantin Galbenu,

1

Ho delineato la mia comprensione approssimativa di come un'app ES / CQRS dovrebbe apparire contestualizzata a un caso d'uso bancario semplificato (prelievo di denaro).

Vicino. Il problema è che la logica per aggiornare il tuo "aggregato" è in una posizione strana.

L'implementazione più comune è che il modello di dati che il gestore dei comandi mantiene in memoria e il flusso di eventi nell'archivio eventi vengono mantenuti sincronizzati.

Un semplice esempio da descrivere è il caso in cui il gestore comandi esegue scritture sincrone nell'archivio eventi e aggiorna la sua copia locale del modello se la connessione all'archivio eventi indica che la scrittura è riuscita.

Se il gestore comandi deve risincronizzare con l'archivio eventi (perché il suo modello interno non corrisponde a quello dell'archivio), lo fa caricando la cronologia dall'archivio e ricostruendo il proprio stato interno.

In altre parole, le frecce 2 e 3 (se presenti) sarebbero normalmente collegate al negozio di eventi, non a un negozio aggregato.

inserisci l'ID della versione aggregata insieme all'evento nell'archivio eventi, quindi in caso di mancata corrispondenza della versione in caso di modifica, non accade nulla

Le variazioni di questo sono il solito caso - piuttosto che aggiungere al flusso nel flusso di eventi, in genere METTIAMO in una posizione specifica nel flusso; se tale operazione non è compatibile con lo stato dell'archivio, la scrittura ha esito negativo e il servizio può scegliere la modalità di errore appropriata (errore nel client, riprovare, unire ....). L'uso delle scritture idempotenti risolve una serie di problemi nella messaggistica distribuita, ma ovviamente richiede un negozio che supporti una scrittura idempotente.


Hmm penso di aver frainteso il componente del negozio di eventi allora. Ho pensato che tutto dovrebbe attraversarlo e viene trasmesso in streaming. Cosa succede se il mio negozio di eventi è un kafka ed è di sola lettura? Non posso permettermi ai passaggi 2 e 3 di ripassare tutti i messaggi. Sembra che nel complesso la mia visione corrisponda a questa: medium.com/technology-learning/…
Louis F.,
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.