Utilizzo di un RDBMS come archiviazione di origine degli eventi


119

Se stavo utilizzando un RDBMS (ad esempio SQL Server) per memorizzare i dati di origine degli eventi, come potrebbe apparire lo schema?

Ho visto alcune variazioni di cui si parla in senso astratto, ma niente di concreto.

Ad esempio, supponiamo che uno abbia un'entità "Prodotto" e le modifiche a quel prodotto potrebbero avere la forma di: Prezzo, Costo e Descrizione. Sono confuso sul fatto che io:

  1. Avere una tabella "ProductEvent", che ha tutti i campi per un prodotto, dove ogni modifica significa un nuovo record in quella tabella, più "chi, cosa, dove, perché, quando e come" (WWWWWH) come appropriato. Quando il costo, il prezzo o la descrizione vengono modificati, viene aggiunta una riga completamente nuova per rappresentare il prodotto.
  2. Memorizza costo, prezzo e descrizione del prodotto in tabelle separate unite alla tabella Prodotto con una relazione di chiave esterna. Quando si verificano modifiche a queste proprietà, scrivere nuove righe con WWWWWH come appropriato.
  3. Memorizza WWWWWH, più un oggetto serializzato che rappresenta l'evento, in una tabella "ProductEvent", il che significa che l'evento stesso deve essere caricato, de-serializzato e riprodotto nuovamente nel codice dell'applicazione per ricostruire lo stato dell'applicazione per un determinato prodotto .

In particolare mi preoccupo dell'opzione 2 sopra. Portata all'estremo, la tabella del prodotto sarebbe quasi una tabella per proprietà, dove caricare lo stato dell'applicazione per un determinato prodotto richiederebbe il caricamento di tutti gli eventi per quel prodotto da ciascuna tabella degli eventi del prodotto. Questa esplosione da tavolo mi ha un cattivo odore.

Sono sicuro che "dipende", e sebbene non ci sia una singola "risposta corretta", sto cercando di avere un'idea di ciò che è accettabile e di ciò che è totalmente non accettabile. Sono anche consapevole del fatto che NoSQL può aiutare qui, dove gli eventi possono essere archiviati su una radice aggregata, il che significa solo una singola richiesta al database per ottenere gli eventi da cui ricostruire l'oggetto, ma non stiamo usando un database NoSQL al momento così sto cercando alternative.


2
Nella sua forma più semplice: [Event] {AggregateId, AggregateVersion, EventPayload}. Non è necessario il tipo di aggregazione, ma è POSSIBILE memorizzarlo facoltativamente. Non è necessario il tipo di evento, ma è POSSIBILE memorizzarlo facoltativamente. È un lungo elenco di cose che sono successe, qualsiasi altra cosa è solo ottimizzazione.
Yves Reynhout

7
Sicuramente stai lontano da # 1 e # 2. Serializza tutto in un BLOB e memorizzalo in questo modo.
Jonathan Oliver

Risposte:


109

L'archivio eventi non dovrebbe aver bisogno di conoscere i campi o le proprietà specifici degli eventi. Altrimenti ogni modifica del tuo modello comporterebbe la necessità di migrare il tuo database (proprio come nella buona vecchia persistenza basata sullo stato). Pertanto non consiglierei affatto le opzioni 1 e 2.

Di seguito è riportato lo schema utilizzato in Ncqrs . Come puoi vedere, la tabella "Eventi" memorizza i dati relativi come CLOB (cioè JSON o XML). Ciò corrisponde alla tua opzione 3 (solo che non esiste una tabella "ProductEvents" perché hai solo bisogno di una tabella generica "Events". In Ncqrs la mappatura alle tue radici aggregate avviene tramite la tabella "EventSources", dove ogni EventSource corrisponde a un effettivo Radice aggregata.)

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

Il meccanismo di persistenza SQL dell'implementazione di Event Store di Jonathan Oliver consiste fondamentalmente in una tabella chiamata "Commits" con un campo BLOB "Payload". Questo è più o meno lo stesso di Ncqrs, solo che serializza le proprietà dell'evento in formato binario (che, ad esempio, aggiunge il supporto per la crittografia).

Greg Young consiglia un approccio simile, come ampiamente documentato sul sito web di Greg .

Lo schema della sua tabella prototipica "Eventi" recita:

Table Events
    AggregateId [Guid],
    Data [Blob],
    SequenceNumber [Long],
    Version [Int]

9
Bella risposta! Uno degli argomenti principali su cui continuo a leggere per utilizzare EventSourcing è la possibilità di interrogare la cronologia. Come creerò uno strumento di report che sia efficiente nell'esecuzione di query quando tutti i dati interessanti sono serializzati come XML o JSON? Ci sono articoli interessanti alla ricerca di una soluzione basata su tabelle?
Marijn Huizendveld

11
@MarijnHuizendveld probabilmente non vuoi eseguire query sull'archivio eventi stesso. La soluzione più comune sarebbe quella di collegare un paio di gestori di eventi che proiettano gli eventi in un database di reportistica o BI. Il replay della cronologia degli eventi contro questi gestori.
Dennis Traub

1
@Denis Traub, grazie per la tua risposta. Perché non eseguire una query sull'archivio eventi stesso? Temo che diventerà piuttosto complicato / intenso se dobbiamo riprodurre la cronologia completa ogni volta che inventiamo un nuovo caso di BI?
Marijn Huizendveld

1
Ho pensato che a un certo punto avresti dovuto anche avere tabelle oltre all'archivio eventi, per memorizzare i dati dal modello nel suo stato più recente? E che hai diviso il modello in un modello di lettura e un modello di scrittura. Il modello di scrittura va contro il negozio di eventi e il negozio di eventi marziali si aggiorna al modello di lettura. Il modello di lettura contiene le tabelle che rappresentano le entità nel tuo sistema, quindi puoi utilizzare il modello di lettura per creare rapporti e visualizzare. Devo aver frainteso qualcosa.
theBoringCoder

10
@theBoringCoder Sembra che tu abbia Event Sourcing e CQRS confusi o almeno schiacciati nella tua testa. Si trovano spesso insieme ma non sono la stessa cosa. CQRS ti consente di separare i tuoi modelli di lettura e scrittura mentre Event Sourcing ti consente di utilizzare un flusso di eventi come unica fonte di verità nella tua applicazione.
Bryan Anderson

7

Il progetto GitHub CQRS.NET ha alcuni esempi concreti di come potresti fare EventStores con poche tecnologie differenti. Al momento in cui scrivo, esiste un'implementazione in SQL che utilizza Linq2SQL e uno schema SQL che lo accompagna, ce n'è uno per MongoDB , uno per DocumentDB (CosmosDB se sei in Azure) e uno che utilizza EventStore (come menzionato sopra). C'è di più in Azure come l'archiviazione tabelle e l'archiviazione BLOB che è molto simile all'archiviazione di file flat.

Immagino che il punto principale qui sia che sono tutti conformi allo stesso contratto / principale. Tutti memorizzano le informazioni in un unico luogo / contenitore / tabella, usano i metadati per identificare un evento da un altro e "semplicemente" memorizzano l'intero evento così com'era - in alcuni casi serializzato, nelle tecnologie di supporto, così com'era. Quindi, a seconda che tu scelga un database di documenti, un database relazionale o anche un file flat, ci sono diversi modi per raggiungere tutti lo stesso intento di un archivio eventi (è utile se cambi idea in qualsiasi momento e trovi che devi migrare o supportare più di una tecnologia di archiviazione).

In qualità di sviluppatore del progetto posso condividere alcuni spunti su alcune delle scelte che abbiamo fatto.

In primo luogo abbiamo trovato (anche con UUID / GUID univoci invece di numeri interi) per molti motivi gli ID sequenziali si verificano per motivi strategici, quindi il solo fatto di avere un ID non era abbastanza unico per una chiave, quindi abbiamo unito la nostra colonna chiave ID principale con i dati / tipo di oggetto per creare quella che dovrebbe essere una chiave veramente univoca (nel senso della vostra applicazione). So che alcune persone dicono che non è necessario archiviarlo, ma ciò dipenderà dal fatto che si sia greenfield o che si debba coesistere con i sistemi esistenti.

Ci siamo attenuti a un singolo contenitore / tabella / raccolta per motivi di manutenibilità, ma abbiamo giocato con una tabella separata per entità / oggetto. Abbiamo scoperto in pratica che significava che l'applicazione necessitava delle autorizzazioni "CREATE" (che in generale non è una buona idea ... generalmente, ci sono sempre eccezioni / esclusioni) oppure ogni volta che una nuova entità / oggetto è nata o è stata distribuita, nuova contenitori / tavoli / collezioni di stoccaggio necessari da realizzare. Abbiamo riscontrato che ciò era estremamente lento per lo sviluppo locale e problematico per le distribuzioni di produzione. Forse no, ma quella era la nostra esperienza nel mondo reale.

Un'altra cosa da ricordare è che chiedere che l'azione X avvenga può comportare il verificarsi di molti eventi diversi, conoscendo così tutti gli eventi generati da un comando / evento / ciò che è utile. Possono anche riguardare diversi tipi di oggetti, ad esempio spingere "acquista" in un carrello della spesa può attivare gli eventi di account e magazzino. Un'applicazione che consuma potrebbe voler sapere tutto questo, quindi abbiamo aggiunto un CorrelationId. Ciò significava che un consumatore poteva chiedere tutti gli eventi sollevati a seguito della sua richiesta. Lo vedrai nello schema .

In particolare con SQL, abbiamo scoperto che le prestazioni diventavano davvero un collo di bottiglia se gli indici e le partizioni non venivano utilizzati adeguatamente. Ricorda che gli eventi dovranno essere trasmessi in streaming in ordine inverso se utilizzi istantanee. Abbiamo provato alcuni indici diversi e abbiamo scoperto che in pratica erano necessari alcuni indici aggiuntivi per il debug delle applicazioni del mondo reale in produzione. Lo vedrai di nuovo nello schema .

Altri metadati in produzione sono stati utili durante le indagini basate sulla produzione, i timestamp ci hanno fornito informazioni sull'ordine in cui gli eventi sono stati persistenti rispetto a quelli generati. Questo ci ha fornito assistenza su un sistema basato su eventi particolarmente pesanti che ha generato grandi quantità di eventi, fornendoci informazioni sulle prestazioni di cose come le reti e la distribuzione dei sistemi attraverso la rete.


È fantastico, grazie. Si dà il caso, molto tempo dopo aver scritto questa domanda, ne ho costruiti alcuni da solo come parte della mia libreria Inforigami.Regalo su GitHub. RavenDB, SQL Server e EventStore implementazioni. Mi chiedevo di farne uno basato su file, per una risata. :)
Neil Barnwell

1
Saluti. Ho aggiunto la risposta principalmente per gli altri che l'hanno incontrata in tempi più recenti e condividono alcune delle lezioni apprese, piuttosto che solo il risultato.
cdmdotnet

3

Beh, potresti dare un'occhiata a Datomic.

Datomic è un database di fatti flessibili e basati sul tempo , che supporta query e join, con scalabilità elastica e transazioni ACID.

Ho scritto una risposta dettagliata qui

Puoi guardare un discorso di Stuart Halloway che spiega il design di Datomic qui

Poiché Datomic archivia i fatti in tempo, puoi usarlo per casi d'uso di sourcing di eventi e molto altro ancora.


2

Penso che la soluzione (1 e 2) possa diventare un problema molto rapidamente man mano che il tuo modello di dominio si evolve. Vengono creati nuovi campi, alcuni cambiano significato e altri non possono più essere utilizzati. Alla fine la tua tabella avrà dozzine di campi nullable e il caricamento degli eventi sarà un pasticcio.

Inoltre, ricorda che l'archivio eventi deve essere utilizzato solo per le scritture, devi solo interrogarlo per caricare gli eventi, non le proprietà dell'aggregato. Sono cose separate (questa è l'essenza del CQRS).

Soluzione 3 ciò che le persone di solito fanno, ci sono molti modi per ottenerla.

Ad esempio, EventFlow CQRS quando viene utilizzato con SQL Server crea una tabella con questo schema:

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
)

dove:

  • GlobalSequenceNumber : semplice identificazione globale, può essere utilizzata per ordinare o identificare gli eventi mancanti quando si crea la proiezione (readmodel).
  • BatchId : un'identificazione del gruppo di eventi che sono stati inseriti atomicamente (TBH, non ho idea del motivo per cui sarebbe utile)
  • AggregateId : identificazione dell'aggregato
  • Dati : evento serializzato
  • Metadati : altre informazioni utili dall'evento (ad es. Tipo di evento utilizzato per deserializzare, timestamp, id originatore dal comando, ecc.)
  • AggregateSequenceNumber : numero di sequenza all'interno dello stesso aggregato (questo è utile se non puoi avere scritture che avvengono fuori ordine, quindi usi questo campo per una concorrenza ottimistica)

Tuttavia, se stai creando da zero, ti consiglio di seguire il principio YAGNI e di creare con i campi minimi richiesti per il tuo caso d'uso.


Direi che BatchId potrebbe essere potenzialmente correlato a CorrelationId e CausationId. Utilizzato per capire cosa ha causato gli eventi e metterli insieme se necessario.
Daniel Park,

Potrebbe essere. Comunque sia così, avrebbe senso fornire un modo per personalizzarlo (ad es. Impostandolo come id della richiesta), ma il framework non lo fa.
Fabio Marreco

1

Un possibile suggerimento è che il design seguito da "Dimensione che cambia lentamente" (tipo = 2) dovrebbe aiutarti a coprire:

  • ordine degli eventi (tramite chiave surrogata)
  • durabilità di ogni stato (valido dal - valido fino al)

Anche la funzione di piegatura a sinistra dovrebbe essere implementata, ma è necessario pensare alla futura complessità della query.


1

Penso che questa sarebbe una risposta tardiva, ma vorrei sottolineare che l'utilizzo di RDBMS come archiviazione di origine degli eventi è del tutto possibile se i requisiti di throughput non sono elevati. Vorrei solo mostrarti esempi di un libro mastro di approvvigionamento di eventi che ho costruito per illustrare.

https://github.com/andrewkkchan/client-ledger-service Quanto sopra è un servizio web di registro di sourcing di eventi. https://github.com/andrewkkchan/client-ledger-core-db E quanto sopra uso RDBMS per calcolare gli stati in modo da poter godere di tutti i vantaggi derivanti da un RDBMS come il supporto delle transazioni. https://github.com/andrewkkchan/client-ledger-core-memory E ho un altro consumatore da elaborare in memoria per gestire i burst.

Si potrebbe obiettare che l'archivio eventi effettivo di cui sopra vive ancora a Kafka, poiché RDBMS è lento per l'inserimento, specialmente quando l'inserimento è sempre in coda.

Spero che il codice ti aiuti a darti un'illustrazione a parte le ottime risposte teoriche già fornite per questa domanda.


Grazie. Da tempo ho costruito un'implementazione basata su SQL. Non sono sicuro del motivo per cui un RDBMS è lento per gli inserimenti a meno che tu non abbia fatto una scelta inefficiente per una chiave cluster da qualche parte. Solo aggiunta dovrebbe andare bene.
Neil Barnwell
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.