Usar un RDBMS como almacenamiento de origen de eventos

119

Si estuviera usando un RDBMS (por ejemplo, SQL Server) para almacenar datos de origen de eventos, ¿cómo sería el esquema?

He visto algunas variaciones de las que se habla en un sentido abstracto, pero nada concreto.

Por ejemplo, digamos que uno tiene una entidad "Producto", y los cambios en ese producto podrían venir en forma de: Precio, Costo y Descripción. Estoy confundido acerca de si:

  1. Tener una tabla "ProductEvent", que tenga todos los campos de un producto, donde cada cambio significa un nuevo registro en esa tabla, más "quién, qué, dónde, por qué, cuándo y cómo" (WWWWWH) según corresponda. Cuando se cambia el costo, el precio o la descripción, se agrega una fila completamente nueva para representar el Producto.
  2. Almacene el costo, el precio y la descripción del producto en tablas separadas unidas a la tabla Producto con una relación de clave externa. Cuando se produzcan cambios en esas propiedades, escriba nuevas filas con WWWWWH según corresponda.
  3. Almacenar WWWWWH, más un objeto serializado que representa el evento, en una tabla "ProductEvent", lo que significa que el evento en sí debe cargarse, eliminarse de la serialización y reproducirse en el código de mi aplicación para reconstruir el estado de la aplicación para un Producto dado. .

Particularmente me preocupa la opción 2 anterior. Llevada al extremo, la tabla de productos sería casi una tabla por propiedad, donde cargar el Estado de la aplicación para un producto dado requeriría cargar todos los eventos para ese producto desde cada tabla de eventos de producto. Esta explosión de mesa me huele mal.

Estoy seguro de que "depende", y aunque no hay una única "respuesta correcta", estoy tratando de tener una idea de lo que es aceptable y lo que es totalmente inaceptable. También soy consciente de que NoSQL puede ayudar aquí, donde los eventos podrían almacenarse en una raíz agregada, lo que significa que solo una solicitud a la base de datos para obtener los eventos para reconstruir el objeto, pero no estamos usando una base de datos NoSQL en el momento, así que estoy buscando alternativas.

Neil Barnwell
fuente
2
En su forma más simple: [Evento] {AggregateId, AggregateVersion, EventPayload}. No es necesario el tipo de agregado, pero PODRÍA almacenarlo opcionalmente. No es necesario el tipo de evento, pero PODRÍA almacenarlo opcionalmente. Es una larga lista de cosas que han sucedido, cualquier otra cosa es solo optimización.
Yves Reynhout
7
Definitivamente manténgase alejado de los números 1 y 2. Serialice todo en un blob y guárdelo de esa manera.
Jonathan Oliver

Respuestas:

109

La tienda de eventos no debería necesitar conocer los campos o propiedades específicos de los eventos. De lo contrario, cada modificación de su modelo resultaría en tener que migrar su base de datos (al igual que en la persistencia basada en el estado a la antigua). Por lo tanto, no recomendaría la opción 1 y 2 en absoluto.

A continuación se muestra el esquema que se utiliza en Ncqrs . Como puede ver, la tabla "Eventos" almacena los datos relacionados como un CLOB (es decir, JSON o XML). Esto corresponde a su opción 3 (Solo que no hay una tabla "ProductEvents" porque solo necesita una tabla "Eventos" genérica. En Ncqrs, la asignación a sus Raíces agregadas se realiza a través de la tabla "EventSources", donde cada EventSource corresponde a un Raíz agregada.)

Table Events:
    Id [uniqueidentifier] NOT NULL,
    TimeStamp [datetime] NOT NULL,

    Name [varchar](max) NOT NULL,
    Version [varchar](max) NOT NULL,

    EventSourceId [uniqueidentifier] NOT NULL,
    Sequence [bigint], 

    Data [nvarchar](max) NOT NULL

Table EventSources:
    Id [uniqueidentifier] NOT NULL, 
    Type [nvarchar](255) NOT NULL, 
    Version [int] NOT NULL

El mecanismo de persistencia SQL de la implementación de Event Store de Jonathan Oliver consiste básicamente en una tabla llamada "Commits" con un campo BLOB "Payload". Esto es más o menos lo mismo que en Ncqrs, solo que serializa las propiedades del evento en formato binario (que, por ejemplo, agrega soporte de cifrado).

Greg Young recomienda un enfoque similar, ampliamente documentado en el sitio web de Greg .

El esquema de su tabla prototípica de "Eventos" dice:

Table Events
    AggregateId [Guid],
    Data [Blob],
    SequenceNumber [Long],
    Version [Int]
Dennis Traub
fuente
9
¡Buena respuesta! Uno de los principales argumentos que sigo leyendo para usar EventSourcing es la capacidad de consultar el historial. ¿Cómo voy a crear una herramienta de informes que sea eficiente para realizar consultas cuando todos los datos interesantes se serializan como XML o JSON? ¿Hay algún artículo interesante que busque una solución basada en tablas?
Marijn Huizendveld
11
@MarijnHuizendveld probablemente no desee consultar el almacén de eventos en sí. La solución más común sería conectar un par de controladores de eventos que proyectan los eventos en una base de datos de informes o BI. La reproducción del historial de eventos contra estos controladores.
Dennis Traub
1
@Denis Traub gracias por tu respuesta. ¿Por qué no consultar en la propia tienda de eventos? Me temo que se volverá bastante complicado / intenso si tenemos que reproducir el historial completo cada vez que se nos ocurra un nuevo caso de BI.
Marijn Huizendveld
1
Pensé que en algún momento se suponía que también tendrías tablas además de la tienda de eventos, para almacenar datos del modelo en su último estado. Y que divida el modelo en un modelo de lectura y un modelo de escritura. El modelo de escritura va en contra de la tienda de eventos, y las marciales de la tienda de eventos se actualizan al modelo de lectura. El modelo de lectura contiene las tablas que representan las entidades en su sistema, por lo que puede usar el modelo de lectura para hacer informes y ver. Debo haber entendido mal algo.
theBoringCoder
10
@theBoringCoder Parece que tienes el abastecimiento de eventos y CQRS confundidos o al menos triturados en tu cabeza. Con frecuencia se encuentran juntos pero no son lo mismo. CQRS le permite separar sus modelos de lectura y escritura, mientras que Event Sourcing le pide que use un flujo de eventos como la única fuente de verdad en su aplicación.
Bryan Anderson
7

El proyecto CQRS.NET de GitHub tiene algunos ejemplos concretos de cómo podría hacer EventStores en algunas tecnologías diferentes. En el momento de escribir este artículo, hay una implementación en SQL que usa Linq2SQL y un esquema SQL que lo acompaña, hay uno para MongoDB , uno para DocumentDB (CosmosDB si está en Azure) y otro que usa EventStore (como se mencionó anteriormente). Hay más en Azure como Table Storage y Blob Storage, que es muy similar al almacenamiento de archivos planos.

Supongo que el punto principal aquí es que todos se ajustan al mismo principio / contrato. Todos almacenan información en un solo lugar / contenedor / tabla, usan metadatos para identificar un evento de otro y 'simplemente' almacenan el evento completo tal como estaba, en algunos casos serializado, en tecnologías de soporte, como estaba. Entonces, dependiendo de si elige una base de datos de documentos, una base de datos relacional o incluso un archivo plano, hay varias formas diferentes de alcanzar la misma intención de una tienda de eventos (es útil si cambia de opinión en cualquier momento y descubre que necesita migrar o brindar soporte más de una tecnología de almacenamiento).

Como desarrollador del proyecto, puedo compartir algunas ideas sobre algunas de las decisiones que tomamos.

En primer lugar, encontramos (incluso con UUID / GUID únicos en lugar de enteros) por muchas razones, las ID secuenciales ocurren por razones estratégicas, por lo que tener una ID no era lo suficientemente única para una clave, por lo que fusionamos nuestra columna de clave de ID principal con los datos / tipo de objeto para crear lo que debería ser una clave verdaderamente única (en el sentido de su aplicación). Sé que algunas personas dicen que no es necesario almacenarlo, pero eso dependerá de si es totalmente nuevo o si tiene que coexistir con los sistemas existentes.

Nos quedamos con un solo contenedor / tabla / colección por razones de mantenimiento, pero jugamos con una tabla separada por entidad / objeto. En la práctica, descubrimos que eso significaba que la aplicación necesitaba permisos de "CREAR" (lo que en general no es una buena idea ... en general, siempre hay excepciones / exclusiones) o cada vez que una nueva entidad / objeto entra en existencia o se implementa, una nueva se necesitaban hacer contenedores / mesas / colecciones de almacenamiento. Descubrimos que esto era dolorosamente lento para el desarrollo local y problemático para las implementaciones de producción. Puede que no, pero esa fue nuestra experiencia en el mundo real.

Otra cosa para recordar es que pedir la acción X puede resultar en que ocurran muchos eventos diferentes, conociendo así todos los eventos generados por un comando / evento / lo que sea útil. También pueden estar en diferentes tipos de objetos, por ejemplo, presionar "comprar" en un carrito de compras puede activar eventos de cuenta y almacenamiento. Es posible que una aplicación consumidora desee saber todo esto, por lo que agregamos un CorrelationId. Esto significaba que un consumidor podía solicitar todos los eventos planteados como resultado de su solicitud. Verá eso en el esquema .

Específicamente con SQL, descubrimos que el rendimiento realmente se convertía en un cuello de botella si los índices y las particiones no se usaban adecuadamente. Recuerde que los eventos deberán transmitirse en orden inverso si está utilizando instantáneas. Probamos algunos índices diferentes y descubrimos que, en la práctica, se necesitaban algunos índices adicionales para depurar aplicaciones del mundo real en producción. Nuevamente lo verá en el esquema .

Otros metadatos en producción fueron útiles durante las investigaciones basadas en la producción, las marcas de tiempo nos dieron una idea del orden en que los eventos persistieron frente a los que se produjeron. Eso nos brindó algo de ayuda en un sistema particularmente impulsado por eventos que generó grandes cantidades de eventos, brindándonos información sobre el rendimiento de cosas como las redes y la distribución de los sistemas en la red.

cdmdotnet
fuente
Eso es genial gracias. Da la casualidad de que hace mucho tiempo que escribí esta pregunta, yo mismo construí algunas como parte de mi biblioteca Inforigami.Regalo en github. Implementaciones de RavenDB, SQL Server y EventStore. Me preguntaba si haría uno basado en archivos, para reírme. :)
Neil Barnwell
1
Salud. Agregué la respuesta principalmente para otros que la han encontrado en tiempos más recientes y comparten algunas de las lecciones aprendidas, en lugar de solo el resultado.
cdmdotnet
3

Bueno, quizás quieras echarle un vistazo a Datomic.

Datomic es una base de datos flexible, basada en el tiempo , que admite consultas y uniones, con escalabilidad elástica y transacciones ACID.

Escribí una respuesta detallada aquí

Puedes ver una charla de Stuart Halloway explicando el diseño de Datomic aquí

Dado que Datomic almacena datos a tiempo, puede usarlo para casos de uso de abastecimiento de eventos y mucho más.

kisai
fuente
2

Creo que la solución (1 y 2) puede convertirse en un problema muy rápidamente a medida que evoluciona su modelo de dominio. Se crean nuevos campos, algunos cambian de significado y algunos pueden dejar de utilizarse. Eventualmente, su tabla tendrá docenas de campos que aceptan valores NULL y cargar los eventos será un desastre.

Además, recuerde que el almacén de eventos debe usarse solo para escrituras, solo lo consulta para cargar los eventos, no las propiedades del agregado. Son cosas separadas (esa es la esencia de CQRS).

Solución 3 lo que la gente suele hacer, hay muchas formas de lograrlo.

Como ejemplo, EventFlow CQRS cuando se usa con SQL Server crea una tabla con este esquema:

CREATE TABLE [dbo].[EventFlow](
    [GlobalSequenceNumber] [bigint] IDENTITY(1,1) NOT NULL,
    [BatchId] [uniqueidentifier] NOT NULL,
    [AggregateId] [nvarchar](255) NOT NULL,
    [AggregateName] [nvarchar](255) NOT NULL,
    [Data] [nvarchar](max) NOT NULL,
    [Metadata] [nvarchar](max) NOT NULL,
    [AggregateSequenceNumber] [int] NOT NULL,
 CONSTRAINT [PK_EventFlow] PRIMARY KEY CLUSTERED 
(
    [GlobalSequenceNumber] ASC
)

dónde:

  • GlobalSequenceNumber : Identificación global simple, puede usarse para ordenar o identificar los eventos faltantes cuando crea su proyección (readmodel).
  • BatchId : una identificación del grupo de eventos que se insertaron atómicamente (TBH, no tengo idea de por qué esto sería útil)
  • AggregateId : identificación del agregado
  • Datos : evento serializado
  • Metadatos : otra información útil del evento (por ejemplo, el tipo de evento utilizado para deserializar, marca de tiempo, identificación del originador del comando, etc.)
  • AggregateSequenceNumber : número de secuencia dentro del mismo agregado (esto es útil si no puede hacer que las escrituras sucedan fuera de orden, por lo que usa este campo para una concurrencia optimista)

Sin embargo, si está creando desde cero, le recomiendo seguir el principio YAGNI y crear con los campos mínimos requeridos para su caso de uso.

Fabio Marreco
fuente
Yo diría que BatchId podría estar relacionado potencialmente con CorrelationId y CausationId. Se usa para averiguar qué causó los eventos y unirlos si es necesario.
Daniel Park
Podría ser. Sin embargo, si esto es así, tendría sentido proporcionar una forma de personalizarlo (por ejemplo, estableciendo como el ID de la solicitud), pero el marco no lo hace.
Fabio Marreco
1

Una posible pista es el diseño seguido de "Dimensión que cambia lentamente" (tipo = 2) que debería ayudarlo a cubrir:

  • orden de los eventos que ocurren (a través de clave sustituta)
  • durabilidad de cada estado (válido desde - válido hasta)

La función de plegado a la izquierda también debería estar bien para implementar, pero debe pensar en la complejidad de la consulta futura.

Viktor Nakonechnyy
fuente
1

Creo que esta sería una respuesta tardía, pero me gustaría señalar que el uso de RDBMS como almacenamiento de origen de eventos es totalmente posible si su requisito de rendimiento no es alto. Solo les mostraría ejemplos de un libro mayor de fuentes de eventos que construí para ilustrar.

https://github.com/andrewkkchan/client-ledger-service El anterior es un servicio web de registro de fuentes de eventos. https://github.com/andrewkkchan/client-ledger-core-db Y lo anterior, uso RDBMS para calcular estados para que pueda disfrutar de todas las ventajas que ofrece un RDBMS como el soporte de transacciones. https://github.com/andrewkkchan/client-ledger-core-memory Y tengo otro consumidor para procesar en la memoria para manejar ráfagas.

Uno podría argumentar que el almacén de eventos real anterior todavía vive en Kafka, ya que RDBMS es lento para insertar, especialmente cuando la inserción siempre se agrega.

Espero que el código le ayude a dar una ilustración además de las muy buenas respuestas teóricas que ya se han proporcionado para esta pregunta.

Andrew Chan
fuente
Gracias. Hace mucho que construí una implementación basada en SQL. No estoy seguro de por qué un RDBMS es lento para inserciones a menos que haya tomado una decisión ineficaz para una clave agrupada en algún lugar. Añadir solo debería estar bien.
Neil Barnwell