¿Cómo mandar eventos al bus de mensajes y escribir en la base de datos de forma Transaccional ?

Ejecutar una operación transaccional se refiere a la ejecución de tareas de escritura en una transacción de forma atómica dentro de un sistema de base de datos. La atomicidad es una de las propiedades fundamentales de las transacciones, significa que todas las operaciones dentro de una transacción se ejecutan como una unidad indivisible. Esto implica que todas las operaciones deben realizarse correctamente o ninguna de ellas se realizará.

En el contexto de bases de datos, una transacción típicamente involucra una serie de operaciones, como inserciones, actualizaciones o eliminaciones de datos. La escritura transaccional asegura que, si una operación de escritura falla por alguna razón (por ejemplo, un error, una violación de integridad, etc.), todas las operaciones realizadas hasta ese punto se deshacen o “rollbackean”. Si todas las operaciones son exitosas, se confirma la transacción y los cambios se hacen permanentes.

Algunos puntos clave sobre la escritura transaccional en bases de datos incluyen:

  • Atomicidad: Todas las operaciones dentro de una transacción se consideran como una única unidad atómica. Si alguna operación falla, se revierten todas las operaciones anteriores.
  • Consistencia: Después de que una transacción se haya completado con éxito, la base de datos debe estar en un estado consistente. Esto significa que debe cumplir con todas las restricciones de integridad y reglas definidas.
  • Aislamiento: Las transacciones deben ejecutarse de manera aislada entre sí. Cada transacción no debe interferir con otras transacciones concurrentes.
  • Durabilidad: Una vez que una transacción se ha confirmado con éxito, los cambios realizados en la base de datos deben ser permanentes y resistir cualquier fallo del sistema.

El uso de escritura transaccional es fundamental para mantener la integridad y la coherencia de los datos en las aplicaciones.

En los microservicios y sistemas distribuidos en general existen diversos sistemas de persistencia, si la arquitectura fue diseñada de acuerdo al principio de una base de datos por servicio, entonces tendremos tantas bases de datos como servicios componen la plataforma, como mínimo. Este principio complejiza mantener la consistencia cuando al insertar un dato en una base de datos el dato es requerido en una tabla de otra base de datos. No existe forma de escribir transaccionalmente en dos bases de datos diferentes, y tampoco conviene en los microservicios que un servicio acceda directamente la base de datos de otro (en otro artículo hablaremos de este tema).

La forma adecuada para que el dato llegue a todas las bases de datos una vez se escriba en una de ellas, es mediante el paso de eventos usando el estilo de comunicación Publicador / Suscriptor.

El patrón Transactional Outbox

El gran reto de este enfoque radica en como garantizar la atomicidad de escribir en la base de datos y a la vez publicar el mensaje en el bus de eventos para que los consumidores puedan escribirlos en sus sistemas de persistencia correspondientes.

Repasemos un par de posibles escenarios:

  • A) Escribimos en la base datos actual de forma exitosa, pero al escribir en el bus de eventos ocurrió un error no recuperable de conexión.
  • B) Enviamos el mensaje al bus de eventos, pero al intentar escribir en la base de datos ocurre un error de constraint por llave única y no se puede completar la operación.

Pudiéramos pensar que el punto B se resuelve haciendo una acción compensatoria enviando un mensaje indicando que se “deshaga” lo ejecutado en el mensaje anterior, pero pueden pasar dos cosas. 1) Qué no podamos enviar por fallos en la red, o 2) que lo enviemos correctamente pero por un problema de prioridades el nuevo mensaje llegue primero.

Intentar enviar y publicar en el bus transaccionalmente es una situación compleja a lo que nos enfrentamos con bastante frecuencia.

Solución al problema

Para resolver este problema se usa la estrategia transactional outbox, veamos con un ejemplo en que consiste.

Supongamos una aplicación diseñada con enfoque microservicios que tiene un servicio llamado food-service encargado de publicar el menú de un restaurante, dicho menú debería estar disponible en otros servicios del restaurante eventualmente. Entonces necesitamos escribir en la tabla del menú y garantizar que llegue a los otros servicios.

La forma de garantizar la atomicidad entre lo que se publica y lo que deben ver los otros sistemas es creando una tabla llamada outbox menú table donde cada vez que se altere el estado del menú en la tabla principal se registre una entrada en esta tabla con la acción correspondiente y el mensaje que debe publicarse en el bus de eventos. Este mecanismo, al escribir dentro de la misma base de datos en dos o más tablas permite garantizar la transaccionalidad.

Hasta este punto tenemos el cambio realizado en la base, pero aún no hemos publicado el mensaje en el bus de eventos, entonces nos falta resolver una parte del problema, que es como leer los mensajes de esa tabla outbox para enviarlo.

Este punto en particular se puede resolver con 2 enfoques diferentes que son:

  • Lectura y publicación de cambios según van apareciendo en la tabla outbox.
  • Lectura de los logs del sistema de persistencia, lo que se conoce sobre Change Data Capture.

Hoy no hablaremos de ellos, pero en próximas entradas lo haremos, no te lo pierdas, únete a nuestra newsletter donde avisamos con un mail semanal (weekly), solo uno ☝️, cero spam.