Reidratação de agregados a partir de uma projeção de "instantâneos" em vez do armazenamento de eventos


13

Então, eu tenho flertado com o Event Sourcing e o CQRS há um tempo, embora nunca tenha tido a oportunidade de aplicar os padrões em um projeto real.

Entendo os benefícios de separar suas preocupações de leitura e gravação e aprecio como o Event Sourcing facilita o projeto de alterações de estado nos bancos de dados "Modelo de Leitura" que são diferentes do seu Armazenamento de Eventos.

O que não sou muito claro é por que você reidrataria seus agregados da própria loja de eventos.

Se projetar alterações nos bancos de dados "ler" é tão fácil, por que nem sempre projetar alterações em um banco de dados "gravar" cujo esquema corresponde perfeitamente ao seu modelo de domínio? Este seria efetivamente um banco de dados de instantâneos.

Eu imagino que isso deve ser bastante comum em aplicativos ES + CQRS em estado selvagem.

Se for esse o caso, o Armazenamento de Eventos é útil apenas ao reconstruir seu banco de dados "gravação" como resultado de alterações de esquema? Ou estou perdendo algo maior?


Não há nada errado em gravar assincronamente em um armazenamento de estado do modelo de gravação e usá-lo exclusivamente para carregar entidades. Os mesmos problemas de consistência exata estão presentes, independentemente de você fazer isso ou não. A chave para corrigir esses problemas de consistência é modelar suas entidades de maneira diferente. Não há nada mágico no Event Sourcing que resolva esses problemas de consistência. A mágica está na modelagem e não se importa. Existem aplicativos específicos que exigem consistência nesse nível, que possuem entidades altamente controversas, não importa como você os modele e exigirão atenção especial independentemente.
Andrew Larsson

Contanto que você possa garantir a entrega de eventos. Para fazer isso, seu aplicativo simplesmente precisa publicar de forma síncrona um evento em um barramento de evento durável. Após a publicação, o trabalho do aplicativo está concluído. O barramento o entregará a vários manipuladores de eventos: um para atualizar o armazenamento de eventos, um para atualizar o armazenamento de estado e outros necessários para atualizar os armazenamentos de leitura. O motivo pelo qual você está usando o Event Sourcing é porque você não se preocupa mais com a consistência imediata. Abrace-o.
Andrew Larsson

Não há motivo para você estar constantemente carregando suas entidades no armazenamento de eventos. Esse não é o seu propósito. Seu objetivo é fornecer um registro permanente e bruto de tudo o que ocorreu no sistema. Armazenamentos de estado de entidade e modelos de leitura não normalizados são para carregamento e leitura.
Andrew Larsson

Respostas:


13

O que não sou muito claro é por que você reidrataria seus agregados da própria loja de eventos.

Porque os "eventos" são o livro de registro.

Se projetar alterações nos bancos de dados "ler" é tão fácil, por que nem sempre projetar alterações em um banco de dados "gravar" cujo esquema corresponde perfeitamente ao seu modelo de domínio? Este seria efetivamente um banco de dados de instantâneos.

Sim; às vezes é uma otimização de desempenho útil usar uma cópia em cache do estado agregado, em vez de regenerar esse estado do zero toda vez. Lembre-se: a primeira regra de otimização de desempenho é "Não". Ele adiciona complexidade extra à solução, e você prefere evitar isso até ter uma motivação comercial atraente.

Se for esse o caso, o Armazenamento de Eventos é útil apenas ao reconstruir seu banco de dados "gravação" como resultado de alterações de esquema? Ou estou perdendo algo maior?

Está faltando algo maior.

O primeiro ponto é que, se você está considerando uma solução de origem de evento, é porque espera que haja valor em preservar o histórico do que aconteceu, ou seja, que você deseja fazer alterações não destrutivas .

É por isso que estamos escrevendo para a loja do evento.

Em particular, isso significa que todas as alterações precisam ser gravadas no armazenamento de eventos.

Os escritores concorrentes podem potencialmente destruir as gravações um do outro ou levar o sistema a um estado não intencional, se não estiverem cientes das edições um do outro. Portanto, a abordagem usual quando você precisa de consistência é direcionar suas gravações para uma posição específica no diário (análoga a uma PUT condicional em uma API HTTP). Uma gravação com falha informa ao escritor que seu entendimento atual do diário está fora de sincronia e que eles devem se recuperar.

Retornar a uma boa posição conhecida e reproduzir quaisquer eventos adicionais, pois esse ponto é uma estratégia de recuperação comum. Essa boa posição conhecida pode ser uma cópia do que está no cache local ou uma representação no seu armazenamento de instantâneos.

No caminho feliz, você pode manter um instantâneo do agregado na memória; você só precisa acessar um armazenamento externo quando não houver uma cópia local disponível.

Além disso, você não precisa ser completamente informado, se tiver acesso ao livro de registro.

Portanto, a abordagem usual ( se estiver usando um repositório de instantâneos) é mantê-lo de forma assíncrona . Dessa forma, se você precisar se recuperar, poderá fazê-lo sem recarregar e reproduzir o histórico inteiro do agregado.

Há muitos casos em que essa complexidade não é interessante, porque agregados refinados com vida útil no escopo geralmente não coletam eventos suficientes para que os benefícios excedam os custos de manutenção de um cache de captura instantânea.

Mas quando é a ferramenta certa para o problema, carregar uma representação obsoleta do agregado no modelo de gravação e atualizá-lo com eventos adicionais é uma coisa perfeitamente razoável a se fazer.


Tudo isso parece razoável. Quando brinquei com uma implementação fictícia do ES há algum tempo, usei o EventStore para garantir consistência, mas também escrevi de forma síncrona para o que você chama de "repositório de instantâneos". Isso significava que o estado atual estava sempre pronto para ler sem ter que reproduzir eventos. Eu suspeitava que isso não daria certo, mas como era apenas um exercício, não me importei.
MetaFight 10/0118

6

Como você não especifica qual seria o objetivo do banco de dados "write", assumirei aqui que o que você quer dizer é o seguinte: ao registrar uma nova atualização em um agregado, em vez de reconstruir o agregado do armazenamento de eventos, você levante-o do banco de dados "write", valide a alteração e emita um evento.

Se é isso que você quer dizer, essa estratégia criará uma condição de inconsistência: se uma nova atualização ocorrer antes que a última tenha a chance de entrar no banco de dados "write", a nova atualização será validada contra dados desatualizados, potencialmente emitindo um evento "impossível" (isto é, "não permitido") e corrompendo o estado do sistema.

Por exemplo, considere um exemplo permanente de reservar assentos em um teatro. Para evitar a reserva dupla, é necessário garantir que o lugar que está sendo reservado ainda não esteja ocupado - é o que você chama de "validação". Para fazer isso, você armazena uma lista de assentos já reservados no banco de dados "write". Quando uma solicitação de reserva é recebida, você verifica se o assento solicitado está na lista e, se não, emite um evento "reservado"; caso contrário, responda com uma mensagem de erro. Em seguida, você executa um processo de projeção, no qual ouve os eventos "reservados" e adiciona os assentos reservados à lista no banco de dados "write".

Normalmente, o sistema funcionaria assim:

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.

No entanto, e se as solicitações forem recebidas muito rapidamente e a etapa 5 acontecer antes da etapa 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.

Agora você tem dois eventos para reservar o mesmo lugar. O estado do sistema está corrompido.

Para impedir que isso aconteça, você nunca deve validar atualizações contra uma projeção. Para validar uma atualização, recrie o agregado do armazenamento de eventos e valide a atualização. Depois disso, você emite um evento, mas use a proteção de carimbo de data e hora para garantir que nenhum novo evento tenha sido emitido desde a última leitura da loja. Se isso falhar, basta tentar novamente.

A reconstrução de agregados a partir do armazenamento de eventos pode acarretar uma penalidade no desempenho. Para atenuar isso, é possível armazenar capturas instantâneas agregadas diretamente no fluxo de eventos, marcado com o ID do evento a partir do qual a captura instantânea foi criada. Dessa forma, você pode reconstruir o agregado carregando a captura instantânea mais recente e reproduzindo apenas os eventos que vieram depois dela, em vez de sempre reproduzir a sequência inteira de eventos desde o início.


Obrigado pela sua resposta (e desculpe-me por demorar tanto para responder). O que você diz sobre a validação no banco de dados de gravação não é necessariamente verdade. Como mencionei em outro comentário, em um exemplo de implementação do ES com o qual eu estava jogando, certifiquei-me de atualizar meu banco de dados de gravação de forma síncrona (e armazenar o concurrencyId / timestamp). Isso me permitiu detectar violações de simultaneidade otimistas sem precisar me preparar do EventStore. As gravações síncronas concedidas, por si só, não protegem contra a corrupção de dados, mas eu também estava fazendo gravações de acesso único (thread único).
MetaFight 10/0118

Então, eu resolvi meus problemas de consistência. No entanto, presumi que isso acontecesse à custa da escalabilidade.
MetaFight 10/0118

A gravação síncrona no banco de dados de gravação ainda apresenta um risco de corrupção: o que acontece se o armazenamento de gravação no evento for bem-sucedido, mas a gravação no banco de dados de gravação falhar?
Fyodor Soikin

1
Se a projeção de leitura falhar, apenas tentará novamente até que seja bem-sucedida. Se travar completamente, será ativado e continuará de onde caiu - em outras palavras, também tente novamente. O efeito observável externo não seria diferente, apenas um pouco lento. Se a projeção continuar falhando e falhar consistentemente, isso significaria que há um bug nela e que precisará ser corrigido. Após a correção, ele retomará a execução do último estado válido. Se todo o banco de dados de leitura for corrompido como resultado do bug, apenas reconstruirei o banco de dados do zero usando o histórico de eventos.
Fyodor Soikin

1
Nenhum dado é perdido, esse é o grande ponto. Os dados podem ficar presos em um formato inconveniente (para leitura) por um tempo, mas nunca são perdidos.
Fyodor Soikin

3

O principal motivo é o desempenho. Você pode armazenar um instantâneo para cada confirmação (commit = os eventos que são gerados por um único comando, geralmente apenas um evento), mas isso é caro. Ao longo da captura instantânea, é necessário armazenar também a confirmação, caso contrário, não seria a origem do evento. E tudo isso deve ser feito atomicamente, tudo ou nada. Sua pergunta é válida apenas se bancos de dados / tabelas / coleções separados forem usados ​​(caso contrário, seria exatamente a origem do evento), portanto você será forçado a usar transações para garantir consistência. As transações não são escaláveis. Um fluxo de eventos somente para acréscimo (o armazenamento de eventos) é a mãe da escalabilidade.

O segundo motivo é o encapsulamento agregado. Você precisa protegê-lo. Isso significa que o Agregado deve estar livre para alterar sua representação interna a qualquer momento. Se você armazená-lo e depender muito dele, terá muita dificuldade com o controle de versão, o que acontecerá com certeza. Na situação em que você usa o instantâneo apenas como otimização, quando o esquema muda, você simplesmente ignora esses instantâneos ( simplesmente ? Acho que não; boa sorte ao determinar que o esquema do Agregado é alterado - incluindo todas as entidades aninhadas e objetos de valor - em um maneira eficiente e de gerenciar isso).


Quando meu esquema agregado é alterado, não seria uma simples questão de reproduzir meus eventos para gerar um banco de dados "de gravação" atualizado?
MetaFight

O problema está detectando essa alteração. Um agregado pode ser muito grande, com muitos arquivos / classes.
Constantin Galbenu

Eu não entendo A mudança aconteceria com uma versão do software. A versão provavelmente viria com um script de banco de dados para regenerar o banco de dados "write".
MetaFight 10/01/19

É muito trabalho a fazer para um script de migração. Enquanto ele é executado, o aplicativo deve estar inativo.
Constantin Galbenu

@MetaFight se o fluxo for muito grande, levará muito tempo para reconstruir o novo esquema agregado ... Estou pensando agora em um instantâneo que é um estado de uma projeção ao vivo que pode estar em execução antes do lançamento do novo agregado schema
Narvalex 15/05/19
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.