Manejo de concurrencia ES / CQRS

20

Recientemente comencé a sumergirme en CQRS / ES porque podría necesitar aplicarlo en el trabajo. Parece muy prometedor en nuestro caso, ya que resolvería muchos problemas.

Esbocé mi comprensión aproximada de cómo debería verse una aplicación ES / CQRS contextualizada a un caso de uso bancario simplificado (retirar dinero).

ES / CQRS

Para resumir, si la persona A retira algo de dinero:

  • se emite un comando
  • el comando se entrega para validación / verificación
  • un evento se envía a un almacén de eventos si la validación se realiza correctamente
  • un agregador retira el evento para aplicar modificaciones en el agregado

Por lo que entendí, el registro de eventos es la fuente de la verdad, ya que es el registro de HECHOS, entonces podemos derivar cualquier proyección de él.


Ahora, lo que no entiendo, en este gran esquema de cosas, es lo que sucede en este caso:

  • regla: un saldo no puede ser negativo
  • la persona A tiene un saldo de 100e
  • la persona A emite una orden de retiro de 100e
  • la validación pasa y se emite el evento MoneyWithdrewEvent of 100e
  • Mientras tanto, la persona A emite otra orden de retirada de 100e
  • el primer MoneyWithdrewEvent no se agregó todavía, por lo tanto, la validación pasa, porque la verificación de validación con el agregado (que aún no se ha actualizado)
  • MoneyWithdrewEvent de 100e se emite otra vez

==> Estamos en un estado inconsistente de un saldo de -100e y el registro contiene 2 MoneyWithdrewEvent

Según tengo entendido, hay varias estrategias para hacer frente a este problema:

  • a) coloque la identificación de versión agregada junto con el evento en el almacén de eventos, por lo que si hay una versión que no coincide con la modificación, no sucede nada
  • b) utilice algunas estrategias de bloqueo, lo que implica que la capa de verificación tiene que crear una de alguna manera

Preguntas relacionadas con las estrategias:

  • a) En este caso, el registro de eventos ya no es la fuente de la verdad, ¿cómo lidiar con él? Además, regresamos al cliente bien, mientras que era totalmente incorrecto permitir el retiro, ¿es mejor en este caso usar bloqueos?
  • b) Bloqueos == puntos muertos, ¿tiene alguna idea sobre las mejores prácticas?

En general, ¿entiendo bien cómo manejar la concurrencia?

Nota: Entiendo que la misma persona que retira dos veces el dinero en tan poco tiempo es imposible, pero tomé un ejemplo simple, para no perderme en detalles

Louis F.
fuente
¿Por qué no actualizar el agregado en el paso 4 en lugar de esperar hasta el paso 7?
Erik Eidt
¿Quiere decir que en este caso, el almacén de eventos es solo un registro que solo se lee al iniciar la aplicación para recrear agregados / otras proyecciones?
Louis F.

Respuestas:

19

Esbocé mi comprensión aproximada de cómo debería verse una aplicación ES / CQRS contextualizada a un caso de uso bancario simplificado (retirar dinero).

Este es el ejemplo perfecto de una aplicación de origen de eventos. Empecemos.

Cada vez que se procesa o reintenta un comando (lo comprenderá, sea paciente) se realizan los siguientes pasos:

  1. el comando llega a un controlador de comando, es decir, un servicio en el Application layer .
  2. el controlador de comandos identifica Aggregatey carga desde el repositorio (en este caso, la carga se realiza haciendo newuna Aggregateinstancia, recuperando todos los eventos emitidos previamente de este agregado y volviéndolos a aplicar al Agregado mismo; la versión del Agregado se almacena para uso posterior; después de que se aplican los eventos, el Agregado está en su estado final, es decir, el saldo de la cuenta corriente se calcula como un número)
  3. el controlador de comandos invoca el método apropiado en Aggregate, como Account::withdrawMoney(100)y recopila los eventos producidos, es decirMoneyWithdrewEvent(AccountId, 100) ; si no hay suficiente dinero en la cuenta (saldo <100), se genera una excepción y se cancela todo; de lo contrario, se realiza el siguiente paso.
  4. el controlador de comandos intenta persistir Aggregateen el repositorio (en este caso, el repositorio es el Event Store); lo hace agregando los nuevos eventos al Event streamif y solo si el versiondel Aggregatetodavía es el que estaba cuando Aggregatese cargó. Si la versión no es la misma, el comando se vuelve a intentar; vaya al paso 1 . Si versiones igual, los eventos se agregan al Event streamy se le proporciona al cliente el Successestado.

Esta comprobación de versión se denomina bloqueo optimista y es un mecanismo de bloqueo general. Otro mecanismo es el bloqueo pesimista. cuando se otras escrituras (como en no iniciado) hasta que se completa la actual.

El termino Event stream es una abstracción en torno a todos los eventos emitidos por el mismo Agregado.

Debe comprender que Event storees solo otro tipo de persistencia donde se almacenan todos los cambios en un agregado, no solo el estado final.

a) En este caso, el registro de eventos ya no es la fuente de la verdad, ¿cómo lidiar con él? Además, regresamos al cliente bien, mientras que era totalmente incorrecto permitir el retiro, ¿es mejor en este caso usar bloqueos?

La tienda de eventos es siempre la fuente de la verdad.

b) Bloqueos == puntos muertos, ¿tiene alguna idea sobre las mejores prácticas?

Al usar el bloqueo optimista no tienes bloqueos, solo ordena volver a intentarlo.

De todos modos, ¡Cerraduras! = Puntos muertos

Constantin Galbenu
fuente
2
Hay algunas optimizaciones con respecto a la carga de un Aggregatelugar en el que no aplica todos los eventos, pero mantiene una instantánea de Aggregatehasta un punto en el pasado y aplica solo los eventos que ocurrieron después de ese punto.
Constantin Galbenu
Ok, creo que mi confusión proviene del hecho de que Event Store == Event Bus (tengo en mente a Kafka), por lo tanto, reconstruir el agregado puede ser costoso, ya que es posible que tengas que volver a leer MUCHOS eventos. En el caso de tener una instantánea de la Aggregate, ¿cuándo debe actualizarse la instantánea? ¿La tienda de instantáneas es la misma que la tienda de eventos o es una vista materializada derivada del bus de eventos?
Louis F.
Existen algunas estrategias para crear la instantánea. Una es hacer una instantánea cada n eventos. Debe almacenar la instantánea junto con los eventos, en el mismo lugar / persistencia / base de datos, en el mismo compromiso. La idea es que la instantánea esté fuertemente relacionada con la versión del Agregado.
Constantin Galbenu
Ok, creo que tengo una visión más clara sobre cómo manejar esto. Ahora última pregunta, ¿cuál es el papel del bus de eventos al final? Si los agregados se actualizan sincrónicamente?
Louis F.
1
Sí, puede usar un RabbitMQ o cualquier canal que desee para enviar los eventos a los modelos leídos de forma asincrónica, pero solo después de que los persista en la tienda de eventos. De hecho, no se realiza la validación de eventos después de que persisten: los eventos representan hechos que sucedieron; a un modelo de lectura le puede gustar o no que algo haya sucedido, pero no puede cambiar el historial.
Constantin Galbenu
1

Esbocé mi comprensión aproximada de cómo debería verse una aplicación ES / CQRS contextualizada a un caso de uso bancario simplificado (retirar dinero).

Cerca. El problema es que la lógica para actualizar su "agregado" está en un lugar extraño.

La implementación más habitual es que el modelo de datos que su controlador de comandos guarda en la memoria y la secuencia de eventos en el almacén de eventos se mantienen sincronizados.

Un ejemplo fácil de describir es el caso en el que el controlador de comandos realiza escrituras sincrónicas en el almacén de eventos y actualiza su copia local del modelo si la conexión al almacén de eventos indica que la escritura se realizó correctamente.

Si el controlador de comandos necesita resincronizarse con el almacén de eventos (porque su modelo interno no coincide con el de la tienda), lo hace cargando el historial de la tienda y reconstruyendo su propio estado interno.

En otras palabras, las flechas 2 y 3 (si están presentes) normalmente estarían conectadas a la tienda de eventos, no a una tienda agregada.

coloque la identificación de versión agregada junto con el evento en el almacén de eventos, de modo que si hay una versión que no coincide con la modificación, no sucede nada

Las variaciones de este son el caso habitual: en lugar de agregarse a la secuencia en la secuencia de eventos, generalmente PONEMOS a una ubicación específica en la secuencia; si esa operación es incompatible con el estado de la tienda, la escritura falla y el servicio puede elegir el modo de falla apropiado (falla al cliente, reintentar, fusionar ...). El uso de escrituras idempotentes resuelve una serie de problemas en la mensajería distribuida, pero, por supuesto, requiere tener una tienda que admita una escritura idempotente.

VoiceOfUnreason
fuente
Hmm, creo que entendí mal el componente de la tienda de eventos entonces. Pensé que todo debería pasar por eso y se transmite. ¿Qué pasa si mi tienda de eventos es un kafka y es de solo lectura? No puedo permitirme en los pasos 2 y 3 revisar todos los mensajes nuevamente. Parece que, en general, mi visión coincidía con esta: medium.com/technology-learning/…
Louis F.