Manipulação de simultaneidade ES / CQRS


20

Recentemente, comecei a mergulhar no CQRS / ES porque talvez eu precise aplicá-lo no trabalho. Parece muito promissor no nosso caso, pois resolveria muitos problemas.

Esbocei meu entendimento aproximado de como um aplicativo ES / CQRS deve parecer contextualizado para um caso de uso bancário simplificado (retirada de dinheiro).

ES / CQRS

Para resumir, se a pessoa A retirar algum dinheiro:

  • um comando é emitido
  • comando é entregue para validação / verificação
  • um evento é enviado para um armazenamento de eventos se a validação for bem-sucedida
  • um agregador remove da fila o evento para aplicar modificações no agregado

Pelo que entendi, o registro de eventos é a fonte da verdade, como é o registro de FACTS, e então podemos derivar qualquer projeção.


Agora, o que eu não entendo, neste grande esquema de coisas, é o que acontece neste caso:

  • regra: um saldo não pode ser negativo
  • a pessoa A tem um saldo de 100e
  • A pessoa A emite um WithdrawCommand de 100e
  • a validação passa e o evento MoneyWithdrewEvent of 100e é emitido
  • Enquanto isso, a pessoa A emite outro WithdrawCommand de 100e
  • o primeiro MoneyWithdrewEvent ainda não foi agregado, portanto, a validação passa, porque a verificação de validação em relação ao agregado (que ainda não foi atualizada)
  • MoneyWithdrewEvent of 100e é emitido outra vez

==> Estamos em um estado inconsistente de um saldo de -100e e o log contém 2 MoneyWithdrewEvent

Pelo que entendi, existem várias estratégias para lidar com esse problema:

  • a) coloque o ID da versão agregada junto com o evento no armazenamento de eventos, para que, se houver uma incompatibilidade de versão na modificação, nada aconteça
  • b) use algumas estratégias de bloqueio, o que implica que a camada de verificação deve, de alguma forma, criar uma

Perguntas relacionadas às estratégias:

  • a) Nesse caso, o log de eventos não é mais a fonte da verdade, como lidar com isso? Além disso, retornamos ao cliente OK, considerando que era totalmente errado permitir a retirada, é melhor neste caso usar bloqueios?
  • b) Bloqueios == impasses, você tem alguma ideia sobre as melhores práticas?

No geral, meu entendimento está correto sobre como lidar com a simultaneidade?

Nota: Entendo que a mesma pessoa que sacar duas vezes o dinheiro em uma janela de tempo tão curta é impossível, mas tomei um exemplo simples, para não me perder nos detalhes


Por que não atualizar o agregado na etapa 4 em vez de esperar até a etapa 7?
Erik Eidt

Então você quer dizer que, nesse caso, o armazenamento de eventos é apenas um log que só é lido ao iniciar o aplicativo para recriar agregações / outras projeções?
Louis F.

Respostas:


19

Esbocei meu entendimento aproximado de como um aplicativo ES / CQRS deve parecer contextualizado para um caso de uso bancário simplificado (retirada de dinheiro).

Este é o exemplo perfeito de um aplicativo de origem de eventos. Vamos começar.

Toda vez que um comando é processado ou repetido (você entenderá, seja paciente), as seguintes etapas são executadas:

  1. o comando alcança um manipulador de comando, ou seja, um serviço no Application layer.
  2. o manipulador de comando identifica Aggregatee o carrega do repositório (nesse caso, o carregamento é realizado por meio de newuma Aggregateinstância, buscando todos os eventos emitidos anteriormente desse agregado e reaplicando-os ao próprio Agregado; a versão Agregada é armazenada para uso posterior; após a aplicação dos eventos, o Agregado está em seu estado final - ou seja, o saldo da conta corrente é calculado como um número)
  3. o manipulador de comando chama o método apropriado em Aggregate, gosta Account::withdrawMoney(100)e coleta os eventos produzidos, ie MoneyWithdrewEvent(AccountId, 100); se não houver dinheiro suficiente na conta (saldo <100), uma exceção será gerada e tudo será abortado; caso contrário, a próxima etapa é executada.
  4. o manipulador de comando tenta persistir no Aggregaterepositório (nesse caso, o repositório é o Event Store); isso é feito anexando os novos eventos ao Event streamif e somente se the versionof Aggregateainda é o que era quando o Aggregatefoi carregado. Se a versão não for a mesma, o comando será tentado novamente - vá para a etapa 1 . Se o versionmesmo for, os eventos serão anexados ao Event streame o cliente receberá o Successstatus.

Essa verificação de versão é chamada de bloqueio otimista e é um mecanismo geral de bloqueio. Um outro mecanismo é o bloqueio pessimista quando outros escritos são bloqueados (como no não iniciado) até que o atual seja concluído.

O termo Event streamé uma abstração em torno de todos os eventos que foram emitidos pelo mesmo agregado.

Você deve entender que esse Event storeé apenas outro tipo de persistência onde são armazenadas todas as alterações em um agregado, não apenas no estado final.

a) Nesse caso, o log de eventos não é mais a fonte da verdade, como lidar com isso? Além disso, retornamos ao cliente OK, considerando que era totalmente errado permitir a retirada, é melhor neste caso usar bloqueios?

A loja de eventos é sempre a fonte da verdade.

b) Bloqueios == impasses, você tem alguma ideia sobre as melhores práticas?

Ao usar o bloqueio otimista, você não tem bloqueios, basta comandar a nova tentativa.

Enfim, Locks! = Deadlocks


2
Existem algumas otimizações em relação ao carregamento de um Aggregatelocal em que você não aplica todos os eventos, mas mantém um instantâneo de Aggregateaté um ponto no passado e aplica apenas os eventos que ocorreram após esse ponto.
Constantin Galbenu

Ok, eu acho que a minha confusão vem do fato de que o armazenamento de eventos == barramento de eventos (eu tenho kafka em mente), portanto, a reconstrução do agregado pode ser um pouco confusa, pois você pode precisar reler muitos eventos. No caso de ter um instantâneo do Aggregate, quando o instantâneo deve ser atualizado? O armazenamento de instantâneos é igual ao armazenamento de eventos ou é uma visualização materializada derivada do barramento de eventos?
Louis F.

Existem algumas estratégias sobre como criar o instantâneo. Uma é fazer um instantâneo a cada n eventos. Você deve armazenar a captura instantânea junto com os eventos, no mesmo local / persistência / banco de dados, no mesmo commit. A ideia é que o instantâneo esteja fortemente relacionado à versão do Agregado.
Constantin Galbenu

Ok, acho que tenho uma visão mais clara de como lidar com isso. Agora, última pergunta, qual é o papel do barramento de eventos no final? Se os agregados são atualizados de forma síncrona?
Louis F.

1
Sim, você pode usar um RabbitMQ ou qualquer outro canal em que deseja enviar os eventos para os modelos de leitura de forma assíncrona, mas somente depois de persistir no armazenamento de eventos. De fato, nenhuma validação de evento é feita após a persistência: os eventos representam fatos que aconteceram; um modelo de leitura pode ou não gostar de que algo aconteceu, mas não pode mudar a história.
Constantin Galbenu

1

Esbocei meu entendimento aproximado de como um aplicativo ES / CQRS deve parecer contextualizado para um caso de uso bancário simplificado (retirada de dinheiro).

Fechar. O problema é que a lógica para atualizar seu "agregado" está em um local estranho.

A implementação mais comum é que o modelo de dados que seu manipulador de comandos mantém na memória e o fluxo de eventos no armazenamento de eventos seja mantido sincronizado.

Um exemplo fácil de descrever é o caso em que o manipulador de comandos faz gravações síncronas no armazenamento de eventos e atualiza sua cópia local do modelo se a conexão com o armazenamento de eventos indicar que a gravação foi bem-sucedida.

Se o manipulador de comandos precisar ressincronizar com o armazenamento de eventos (porque seu modelo interno não corresponde ao da loja), ele fará isso carregando o histórico da loja e reconstruindo seu próprio estado interno.

Em outras palavras, as setas 2 e 3 (se presentes) normalmente seriam conectadas ao armazenamento de eventos, não a um armazenamento agregado.

coloque o ID da versão agregada junto com o evento no repositório de eventos. Se houver uma incompatibilidade de versão na modificação, nada acontecerá

Variações são o caso usual - em vez de anexar ao fluxo no fluxo de eventos, normalmente COLOCAMOS em um local específico no fluxo; se essa operação for incompatível com o estado do armazenamento, a gravação falhará e o serviço poderá escolher o modo de falha apropriado (falha no cliente, tente novamente, mescle ...). O uso de gravações idempotentes resolve vários problemas nas mensagens distribuídas, mas é claro que exige ter um armazenamento que suporte uma gravação idempotente.


Hmm, acho que não entendi o componente da loja de eventos. Eu pensei que tudo deveria passar por isso e ser transmitido. E se minha loja de eventos for kafka e for somente leitura? Não posso me permitir nas etapas 2 e 3 passar por todas as mensagens novamente. Parece que, no geral a minha visão acompanhado esta: medium.com/technology-learning/...
Louis F.
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.