Usando um RDBMS como armazenamento de origem de eventos

119

Se eu estivesse usando um RDBMS (por exemplo, SQL Server) para armazenar dados de origem de eventos, como seria o esquema?

Eu vi algumas variações faladas em um sentido abstrato, mas nada de concreto.

Por exemplo, digamos que alguém tenha uma entidade "Produto" e as alterações nesse produto possam vir na forma de: Preço, Custo e Descrição. Estou confuso sobre se eu:

  1. Tenha uma tabela "ProductEvent", que contém todos os campos de um produto, onde cada alteração significa um novo registro nessa tabela, mais "quem, o quê, onde, por que, quando e como" (WWWWWH) conforme apropriado. Quando o custo, preço ou descrição são alterados, uma nova linha inteira é adicionada para representar o Produto.
  2. Armazene Custo, Preço e Descrição do produto em tabelas separadas unidas à tabela Produto com um relacionamento de chave estrangeira. Quando ocorrerem alterações nessas propriedades, escreva novas linhas com WWWWWH conforme apropriado.
  3. Armazene WWWWWH, mais um objeto serializado que representa o evento, em uma tabela "ProductEvent", o que significa que o próprio evento deve ser carregado, desserializado e reproduzido no código do meu aplicativo para reconstruir o estado do aplicativo para um determinado produto .

Particularmente, me preocupo com a opção 2 acima. Levada ao extremo, a tabela de produtos seria quase uma tabela por propriedade, onde carregar o Estado do aplicativo para um determinado produto exigiria o carregamento de todos os eventos desse produto de cada tabela de eventos do produto. Esta explosão de mesa me cheira mal.

Tenho certeza de que "depende" e, embora não haja uma única "resposta correta", estou tentando sentir o que é aceitável e o que é totalmente não aceitável. Também estou ciente de que o NoSQL pode ajudar aqui, onde os eventos podem ser armazenados em uma raiz agregada, o que significa apenas uma única solicitação ao banco de dados para obter os eventos para reconstruir o objeto, mas não estamos usando um banco de dados NoSQL no momento, então estou procurando alternativas.

Neil Barnwell
fonte
2
Em sua forma mais simples: [Event] {AggregateId, AggregateVersion, EventPayload}. Não há necessidade do tipo de agregado, mas você PODE opcionalmente armazená-lo. Não há necessidade de tipo de evento, mas você PODE opcionalmente armazená-lo. É uma longa lista de coisas que aconteceram, qualquer outra coisa é apenas otimização.
Yves Reynhout de
7
Definitivamente, fique longe de # 1 e # 2. Serialize tudo em uma bolha e armazene-o dessa forma.
Jonathan Oliver

Respostas:

109

O armazenamento de eventos não precisa saber sobre os campos ou propriedades específicos dos eventos. Caso contrário, toda modificação de seu modelo resultaria na migração de seu banco de dados (como na velha persistência baseada em estado). Portanto, eu não recomendaria as opções 1 e 2.

Abaixo está o esquema usado no Ncqrs . Como você pode ver, a tabela "Eventos" armazena os dados relacionados como um CLOB (ou seja, JSON ou XML). Isso corresponde à sua opção 3 (apenas que não existe uma tabela "ProductEvents" porque você só precisa de uma tabela genérica "Eventos". Em Ncqrs o mapeamento para suas raízes agregadas ocorre através da tabela "EventSources", onde cada EventSource corresponde a um Raiz agregada.)

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

O mecanismo de persistência SQL da implementação do Event Store de Jonathan Oliver consiste basicamente em uma tabela chamada "Commits" com um campo BLOB "Payload". É praticamente o mesmo que no Ncqrs, apenas que serializa as propriedades do evento em formato binário (que, por exemplo, adiciona suporte à criptografia).

Greg Young recomenda uma abordagem semelhante, amplamente documentada no site de Greg .

O esquema de sua tabela prototípica de "Eventos" é:

Table Events
    AggregateId [Guid],
    Data [Blob],
    SequenceNumber [Long],
    Version [Int]
Dennis Traub
fonte
9
Boa resposta! Um dos principais argumentos que continuo lendo para usar o EventSourcing é a capacidade de consultar o histórico. Como vou fazer uma ferramenta de relatório que seja eficiente na consulta quando todos os dados interessantes são serializados como XML ou JSON? Há algum artigo interessante procurando uma solução baseada em tabela?
Marijn Huizendveld
11
@MarijnHuizendveld você provavelmente não deseja consultar o armazenamento de eventos em si. A solução mais comum seria conectar alguns manipuladores de eventos que projetam os eventos em um relatório ou banco de dados de BI. A reprodução do histórico de eventos contra esses manipuladores.
Dennis Traub
1
@Denis Traub obrigado pela sua resposta. Por que não consultar o próprio armazenamento de eventos? Temo que ficará bastante confuso / intenso se tivermos que repetir toda a história toda vez que apresentarmos um novo caso de BI.
Marijn Huizendveld
1
Achei que em algum momento você também deveria ter tabelas além do armazenamento de eventos, para armazenar dados do modelo em seu estado mais recente. E que você divide o modelo em um modelo de leitura e um modelo de gravação. O modelo de gravação vai contra o armazenamento de eventos e as atualizações marciais do armazenamento de eventos para o modelo de leitura. O modelo de leitura contém as tabelas que representam as entidades em seu sistema - portanto, você pode usar o modelo de leitura para fazer relatórios e visualizar. Devo ter entendido mal alguma coisa.
theBoringCoder
10
@theBoringCoder Parece que o Event Sourcing e o CQRS estão confusos ou pelo menos misturados em sua cabeça. Eles são freqüentemente encontrados juntos, mas não são a mesma coisa. O CQRS permite que você separe seus modelos de leitura e gravação, enquanto o Event Sourcing faz com que você use um fluxo de eventos como a única fonte de verdade em seu aplicativo.
Bryan Anderson
7

O projeto GitHub CQRS.NET tem alguns exemplos concretos de como você pode fazer EventStores em algumas tecnologias diferentes. No momento da escrita, há uma implementação em SQL usando Linq2SQL e um esquema SQL para acompanhá-lo, há um para MongoDB , um para DocumentDB (CosmosDB se você estiver no Azure) e um usando EventStore (como mencionado acima). Há mais no Azure, como armazenamento de tabela e armazenamento de Blob, que é muito semelhante ao armazenamento de arquivo simples.

Acho que o ponto principal aqui é que todos estão em conformidade com o mesmo princípio / contrato. Todos eles armazenam informações em um único local / contêiner / mesa, usam metadados para identificar um evento de outro e 'apenas' armazenam todo o evento como ele era - em alguns casos serializado, em tecnologias de suporte, como era. Portanto, dependendo se você escolher um banco de dados de documentos, um banco de dados relacional ou até mesmo um arquivo simples, há várias maneiras diferentes de todos alcançarem a mesma intenção de um armazenamento de eventos (é útil se você mudar de ideia a qualquer momento e descobrir que precisa migrar ou dar suporte mais de uma tecnologia de armazenamento).

Como desenvolvedor do projeto, posso compartilhar alguns insights sobre algumas das escolhas que fizemos.

Em primeiro lugar, descobrimos (mesmo com UUIDs / GUIDs únicos em vez de inteiros) por muitos motivos que os IDs sequenciais ocorrem por razões estratégicas, portanto, apenas ter um ID não era exclusivo o suficiente para uma chave, então mesclamos nossa coluna de chave de ID principal com os dados / tipo de objeto para criar o que deve ser uma chave verdadeiramente única (no sentido de seu aplicativo). Eu sei que algumas pessoas dizem que você não precisa armazená-lo, mas isso vai depender se você é um novato ou precisa coexistir com os sistemas existentes.

Ficamos com um único contêiner / tabela / coleção por motivos de manutenção, mas brincamos com uma tabela separada por entidade / objeto. Descobrimos na prática que isso significava que o aplicativo precisava de permissões "CRIAR" (o que geralmente não é uma boa ideia ... geralmente, sempre há exceções / exclusões) ou cada vez que uma nova entidade / objeto surgiu ou foi implantado, novo recipientes / tabelas / coleções de armazenamento precisavam ser feitos. Descobrimos que isso era dolorosamente lento para o desenvolvimento local e problemático para implantações de produção. Você pode não, mas essa foi a nossa experiência no mundo real.

Outra coisa a lembrar é que pedir que a ação X aconteça pode resultar em muitos eventos diferentes ocorrendo, portanto, conhecendo todos os eventos gerados por um comando / evento / o que quer que seja útil. Eles também podem estar em diferentes tipos de objetos, por exemplo, empurrar "comprar" em um carrinho de compras pode acionar eventos de conta e armazenamento. Um aplicativo de consumo pode querer saber tudo isso, então adicionamos um CorrelationId. Isso significava que um consumidor poderia solicitar todos os eventos gerados como resultado de sua solicitação. Você verá isso no esquema .

Especificamente com o SQL, descobrimos que o desempenho realmente se tornava um gargalo se os índices e partições não fossem usados ​​adequadamente. Lembre-se de que os eventos precisarão ser transmitidos em ordem reversa se você estiver usando instantâneos. Tentamos alguns índices diferentes e descobrimos que, na prática, alguns índices adicionais eram necessários para depurar aplicativos do mundo real em produção. Novamente, você verá isso no esquema .

Outros metadados em produção foram úteis durante as investigações baseadas na produção, carimbos de data / hora nos deram uma visão sobre a ordem em que os eventos foram persistidos ou gerados. Isso nos deu alguma assistência em um sistema especialmente orientado a eventos que gerou uma grande quantidade de eventos, nos fornecendo informações sobre o desempenho de coisas como redes e a distribuição de sistemas pela rede.

cdmdotnet
fonte
Isso é ótimo, obrigado. Acontece que, há muito tempo que escrevi esta pergunta, eu mesmo criei algumas como parte da minha biblioteca Inforigami.Regalo no github. Implementações de RavenDB, SQL Server e EventStore. Queria fazer um baseado em arquivo, para rir. :)
Neil Barnwell
1
Felicidades. Acrescentei a resposta principalmente para outros que a encontraram em tempos mais recentes e compartilharam algumas das lições aprendidas, em vez de apenas o resultado.
cdmdotnet
3

Bem, você pode querer dar uma olhada no Datomic.

O Datomic é um banco de dados de fatos flexíveis e baseados em tempo , com suporte a consultas e junções, com escalabilidade elástica e transações ACID.

Eu escrevi uma resposta detalhada aqui

Você pode assistir a uma palestra de Stuart Halloway explicando o design do Datomic aqui

Como o Datomic armazena fatos a tempo, você pode usá-lo para casos de uso de sourcing de eventos e muito mais.

kisai
fonte
2

Acho que a solução (1 e 2) pode se tornar um problema muito rapidamente à medida que seu modelo de domínio evolui. Novos campos são criados, alguns mudam de significado e outros podem não ser mais usados. Eventualmente, sua tabela terá dezenas de campos anuláveis, e carregar os eventos será uma bagunça.

Além disso, lembre-se de que o armazenamento de eventos deve ser usado apenas para gravações, você apenas o consulta para carregar os eventos, não as propriedades do agregado. Eles são coisas separadas (essa é a essência do CQRS).

Solução 3 - o que as pessoas geralmente fazem, há muitas maneiras de conseguir isso.

Como exemplo, EventFlow CQRS quando usado com SQL Server cria uma tabela com este esquema:

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
)

Onde:

  • GlobalSequenceNumber : Identificação global simples, pode ser usada para solicitar ou identificar os eventos ausentes quando você cria sua projeção (modelo de leitura).
  • BatchId : Uma identificação do grupo de eventos que foram inseridos atomicamente (TBH, não tenho ideia de por que isso seria útil)
  • AggregateId : Identificação do agregado
  • Dados : evento serializado
  • Metadados : Outras informações úteis do evento (por exemplo, tipo de evento usado para desserializar, carimbo de data / hora, id do originador do comando, etc.)
  • AggregateSequenceNumber : número de sequência dentro do mesmo agregado (isso é útil se você não pode ter gravações acontecendo fora de ordem, então você usa este campo para simultaneidade otimista)

No entanto, se você estiver criando do zero, recomendo seguir o princípio YAGNI e criar com o mínimo de campos necessários para o seu caso de uso.

Fabio Marreco
fonte
Eu diria que BatchId pode estar potencialmente relacionado a CorrelationId e CausationId. Usado para descobrir o que causou os eventos e agrupá-los, se necessário.
Daniel Park
Poderia ser. Seja como for, faria sentido fornecer uma maneira de personalizá-lo (por exemplo, definindo como o id da solicitação), mas o framework não faz isso.
Fabio Marreco
1

A possível dica é o design seguido por "Dimensão que muda lentamente" (tipo = 2) deve ajudá-lo a cobrir:

  • ordem de eventos ocorrendo (via surrogate key)
  • durabilidade de cada estado (válido de - válido até)

A função de dobra à esquerda também deve ser implementada, mas você precisa pensar na complexidade da consulta futura.

Viktor Nakonechnyy
fonte
1

Acho que essa seria uma resposta tardia, mas gostaria de salientar que usar RDBMS como armazenamento de origem de eventos é totalmente possível se o seu requisito de taxa de transferência não for alto. Gostaria apenas de mostrar exemplos de um livro razão de sourcing de eventos que construí para ilustrar.

https://github.com/andrewkkchan/client-ledger-service O texto acima é um serviço da web do razão de sourcing de eventos. https://github.com/andrewkkchan/client-ledger-core-db E acima, eu uso RDBMS para computar estados para que você possa aproveitar todas as vantagens de um RDBMS como o suporte a transações. https://github.com/andrewkkchan/client-ledger-core-memory E tenho outro consumidor para processar na memória para lidar com bursts.

Alguém poderia argumentar que o armazenamento de eventos real acima ainda vive em Kafka - como RDBMS é lento para inserir, especialmente quando a inserção está sempre anexando.

Espero que o código ajude a fornecer uma ilustração além das ótimas respostas teóricas já fornecidas para esta pergunta.

Andrew Chan
fonte
Obrigado. Há muito tempo desenvolvi uma implementação baseada em SQL. Não sei por que um RDBMS é lento para inserções, a menos que você tenha feito uma escolha ineficiente para uma chave de cluster em algum lugar. Append-only deve servir.
Neil Barnwell