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).
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
fonte
Respostas:
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:
Application layer
.Aggregate
e o carrega do repositório (nesse caso, o carregamento é realizado por meio denew
umaAggregate
instâ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)Aggregate
, gostaAccount::withdrawMoney(100)
e coleta os eventos produzidos, ieMoneyWithdrewEvent(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.Aggregate
repositório (nesse caso, o repositório é oEvent Store
); isso é feito anexando os novos eventos aoEvent stream
if e somente se theversion
ofAggregate
ainda é o que era quando oAggregate
foi carregado. Se a versão não for a mesma, o comando será tentado novamente - vá para a etapa 1 . Se oversion
mesmo for, os eventos serão anexados aoEvent stream
e o cliente receberá oSuccess
status.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 loja de eventos é sempre a fonte da verdade.
Ao usar o bloqueio otimista, você não tem bloqueios, basta comandar a nova tentativa.
Enfim, Locks! = Deadlocks
fonte
Aggregate
local em que você não aplica todos os eventos, mas mantém um instantâneo deAggregate
até um ponto no passado e aplica apenas os eventos que ocorreram após esse ponto.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?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.
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.
fonte