Rehidratar agregados de una proyección de "instantáneas" en lugar de la tienda de eventos

14

Así que he estado coqueteando con Event Sourcing y CQRS durante un tiempo, aunque nunca tuve la oportunidad de aplicar los patrones en un proyecto real.

Entiendo los beneficios de separar sus inquietudes de lectura y escritura, y aprecio cómo Event Sourcing hace que sea fácil proyectar cambios de estado en las bases de datos de "Modelo de lectura" que son diferentes de su Tienda de eventos.

Lo que no tengo muy claro es por qué alguna vez rehidratarías tus agregados de la tienda de eventos.

Si proyectar cambios en bases de datos "leídas" es tan fácil, ¿por qué no siempre proyectar cambios en una base de datos "escrita" cuyo esquema coincida perfectamente con su modelo de dominio? Esto sería efectivamente una base de datos de instantáneas.

Me imagino que esto debe ser bastante común en las aplicaciones ES + CQRS en la naturaleza.

Si este es el caso, ¿es útil el Almacén de eventos al reconstruir su base de datos de "escritura" como resultado de cambios de esquema? ¿O me estoy perdiendo algo más grande?

MetaFight
fuente
No hay nada de malo en escribir asíncronamente en un almacén de estado del modelo de escritura y usarlo exclusivamente para cargar entidades. Los mismos problemas de coherencia exactos están presentes, ya sea que lo haga o no. La clave para solucionar esos problemas de coherencia es modelar sus entidades de manera diferente. No hay nada mágico en el Abastecimiento de eventos que resuelva esos problemas de coherencia. La magia reside en modelar y no preocuparse. Hay aplicaciones específicas que requieren consistencia a ese nivel que tienen entidades altamente polémicas sin importar cómo las modeles y requerirán atención especial independientemente.
Andrew Larsson el
Siempre que pueda garantizar la entrega de eventos. Para hacer esto, su aplicación simplemente necesita publicar sincrónicamente un evento en un bus de eventos duradero. Después de la publicación, el trabajo de la aplicación está completo. Luego, el bus lo entregará a varios controladores de eventos: uno para actualizar la tienda de eventos, otro para actualizar la tienda estatal y cualquier otro que sea necesario para actualizar las tiendas de lectura. La razón por la que está utilizando Event Sourcing es porque ya no le importa la consistencia inmediata. Abrázalo.
Andrew Larsson
No hay ninguna razón por la que deba cargar constantemente sus entidades desde la tienda de eventos. Ese no es su propósito. Su propósito es proporcionar un libro mayor crudo y permanente de todo lo que ha ocurrido en el sistema. Las tiendas de estado de entidad y los modelos de lectura desnormalizados son para cargar y leer.
Andrew Larsson

Respuestas:

14

Lo que no tengo muy claro es por qué alguna vez rehidratarías tus agregados de la tienda de eventos.

Porque los "eventos" son el libro de registro.

Si proyectar cambios en bases de datos "leídas" es tan fácil, ¿por qué no siempre proyectar cambios en una base de datos "escrita" cuyo esquema coincida perfectamente con su modelo de dominio? Esto sería efectivamente una base de datos de instantáneas.

Si; A veces es una optimización de rendimiento útil utilizar una copia en caché del estado agregado, en lugar de regenerar ese estado desde cero cada vez. Recuerde: la primera regla de optimización del rendimiento es "No". Agrega complejidad adicional a la solución, y preferiría evitarlo hasta que tenga una motivación comercial convincente.

Si este es el caso, ¿es útil el Almacén de eventos al reconstruir su base de datos de "escritura" como resultado de cambios de esquema? ¿O me estoy perdiendo algo más grande?

Te estás perdiendo algo más grande.

El primer punto es que si está considerando una solución de origen de eventos, es porque espera que haya valor en preservar el historial de lo que sucedió, es decir que quiere hacer cambios no destructivos .

Por eso estamos escribiendo a la tienda de eventos.

En particular, esto significa que cada cambio debe escribirse en la tienda de eventos.

Los escritores competidores podrían potencialmente destruir las escrituras de los demás o conducir el sistema a un estado no deseado, si no están al tanto de las ediciones de los demás. Entonces, el enfoque habitual cuando necesita consistencia es dirigir sus escritos a una posición específica en el diario (análoga a un PUT condicional en una API HTTP). Una escritura fallida le dice al escritor que su comprensión actual de la revista no está sincronizada y que deberían recuperarse.

Volver a una buena posición conocida y luego reproducir cualquier evento adicional desde ese punto es una estrategia de recuperación común. Esa buena posición conocida puede ser una copia de lo que está en el caché local o una representación en su tienda de instantáneas.

En el camino feliz, puede guardar una instantánea del agregado en la memoria; solo necesita comunicarse con una tienda externa cuando no hay una copia local disponible.

Además, no necesita estar completamente atrapado si tiene acceso al libro de registro.

Entonces, el enfoque habitual ( si se usa un repositorio de instantáneas) es mantenerlo de forma asíncrona . De esa manera, si necesita recuperarse, puede hacerlo sin volver a cargar y reproducir todo el historial del agregado.

Hay muchos casos en los que esta complejidad no es de interés, porque los agregados de grano fino con vidas de alcance generalmente no recopilan suficientes eventos para que los beneficios excedan los costos de mantener un caché de instantáneas.

Pero cuando es la herramienta adecuada para el problema, cargar una representación obsoleta del agregado en el modelo de escritura y luego actualizarlo con eventos adicionales es algo perfectamente razonable.

VoiceOfUnreason
fuente
Todo esto suena razonable. Cuando jugué con una implementación ficticia de ES hace un tiempo, utilicé EventStore para garantizar la coherencia, pero también escribí sincrónicamente en lo que usted llama un "repositorio de instantáneas". Esto significaba que el estado actual siempre estaba listo para leer sin tener que reproducir eventos. Sospeché que esto no escalaría bien, pero como era solo un ejercicio, no me importó.
MetaFight
6

Como no especifica cuál sería el propósito de la base de datos de "escritura", supondré aquí que lo que quiere decir es esto: al registrar una nueva actualización en un agregado, en lugar de reconstruir el agregado desde el almacén de eventos, usted levantarlo de la base de datos "escribir", validar el cambio y emitir un evento.

Si esto es lo que quiere decir, entonces esta estrategia creará una condición para la inconsistencia: si una nueva actualización ocurre antes de que la última tuviera la oportunidad de ingresar a la base de datos de "escritura", la nueva actualización terminará validada contra datos obsoletos, potencialmente emitiendo un evento "imposible" (es decir, "no permitido") y corrompiendo el estado del sistema.

Por ejemplo, considere un ejemplo permanente de reservar asientos en un teatro. Para evitar la doble reserva, debe asegurarse de que el asiento que se está reservando no esté ocupado, esto es lo que llama "validación". Para hacer eso, almacena una lista de asientos ya reservados en la base de datos "escribir". Luego, cuando llega una solicitud de reserva, verifica si el asiento solicitado está en la lista y, si no, emite un evento "reservado", de lo contrario responde con un mensaje de error. Luego ejecuta un proceso de proyección, donde escucha los eventos "reservados" y agrega los asientos reservados a la lista en la base de datos "escribir".

Normalmente, el sistema funcionaría así:

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.

Sin embargo, ¿qué pasa si las solicitudes llegan demasiado rápido y el paso 5 ocurre antes del paso 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.

Ahora tiene dos eventos para reservar el mismo asiento. El estado del sistema está dañado.

Para evitar que esto suceda, nunca debe validar las actualizaciones contra una proyección. Para validar una actualización, reconstruye el agregado desde el almacén de eventos y luego valida la actualización contra él. Después de eso, emite un evento, pero utiliza la protección de marca de tiempo para asegurarse de que no se hayan emitido nuevos eventos desde la última vez que leyó en la tienda. Si esto falla, solo vuelve a intentarlo.

La reconstrucción de agregados de la tienda de eventos puede conllevar una penalización de rendimiento. Para mitigar esto, puede almacenar instantáneas agregadas directamente en la secuencia del evento, etiquetadas con el ID del evento desde el que se creó la instantánea. De esta manera, puede reconstruir el agregado cargando la instantánea más reciente y reproduciendo solo los eventos que vinieron después, en lugar de reproducir siempre todo el flujo de eventos desde el principio de los tiempos.

Fyodor Soikin
fuente
Gracias por su respuesta (y perdón por tardar tanto en responder). Lo que dices sobre la validación contra la base de datos de escritura no es necesariamente cierto. Como mencioné en otro comentario, en un ejemplo de implementación de ES con la que estaba jugando, me aseguré de actualizar mi base de datos de escritura de forma síncrona (y almacenar el concurrenciaId / marca de tiempo). Esto me permitió detectar violaciones de concurrencia optimista sin necesidad de estar listo desde EventStore. Por supuesto, las escrituras síncronas por sí solas no protegen contra la corrupción de datos, pero también estaba haciendo escrituras de acceso único (subproceso único).
MetaFight el
Entonces, tuve mis problemas de consistencia resueltos. Sin embargo, supuse que esto era a expensas de la escalabilidad.
MetaFight el
Escribir sincrónicamente en la base de datos de escritura aún conlleva un peligro de corrupción: ¿qué sucede si su escritura en la tienda de eventos tiene éxito, pero falla su escritura en la base de datos de escritura?
Fyodor Soikin
1
Si la proyección de lectura falla, solo volverá a intentarlo hasta que tenga éxito. Si se bloquea por completo, se despertará y continuará desde donde se estrelló; en otras palabras, también volverá a intentarlo. El efecto observable externo no sería diferente de que solo se ejecutara un poco lento. Si la proyección sigue fallando y fallando constantemente, eso significaría que hay un error, y eso tendrá que repararse. Después de la reparación, se reanudará la ejecución desde el último buen estado. Si toda la base de datos de lectura se corrompe como resultado del error, simplemente reconstruiré la base de datos desde cero utilizando el historial de eventos.
Fyodor Soikin
1
No se pierden datos, ese es el gran punto. Los datos pueden estar atascados en una forma incómoda (para leer) por un tiempo, pero nunca se pierden.
Fyodor Soikin
3

La razón principal es el rendimiento. Puede almacenar una instantánea para cada confirmación (commit = los eventos generados por un solo comando, generalmente solo un evento), pero esto es costoso. A lo largo de la instantánea, debe almacenar también la confirmación, de lo contrario no sería el abastecimiento de eventos. Y todo esto debe hacerse atómicamente, todo o nada. Su pregunta es válida solo si se utilizan bases de datos / tablas / colecciones separadas (de lo contrario, sería exactamente el abastecimiento de eventos), por lo que se ve obligado a usar transacciones para garantizar la coherencia. Las transacciones no son escalables. Una secuencia de eventos de solo agregado (la tienda de eventos) es la madre de la escalabilidad.

La segunda razón es la encapsulación agregada. Necesitas protegerlo. Esto significa que el Agregado debería ser libre de cambiar su representación interna en cualquier momento. Si lo almacena y depende en gran medida de él, tendrá dificultades con el control de versiones, lo que seguramente sucederá. En la situación en la que utiliza la instantánea solo como una optimización, cuando el esquema cambia, simplemente ignora esas instantáneas ( ¿ simplemente ? Realmente no lo creo; buena suerte para determinar que el esquema de Aggregate cambia, incluidas todas las entidades anidadas y los objetos de valor, en un de manera eficiente y gestionando eso).

Constantin Galbenu
fuente
Cuando cambia mi esquema de Agregado, ¿no sería una simple cuestión de reproducir mis eventos para generar una base de datos actualizada de "escritura"?
MetaFight el
El problema es detectar ese cambio. Un agregado podría ser muy grande, con muchos archivos / clases.
Constantin Galbenu
No entiendo. El cambio sucedería con una versión de software. La versión probablemente vendría con un script de base de datos para regenerar la base de datos "escribir".
MetaFight el
Es mucho trabajo hacer para un script de migración. Mientras se ejecuta, la aplicación debe estar inactiva.
Constantin Galbenu
@MetaFight si la transmisión es muy grande, llevará mucho tiempo reconstruir el nuevo esquema agregado ... Ahora estoy pensando en una instantánea que es un estado de una proyección en vivo que podría estar ejecutándose antes del lanzamiento del nuevo agregado esquema
Narvalex