Servidor Couchbase

Transacciones multidocumento: ACID y Couchbase Parte 2

Nota importante: Transacciones Multi-Documento ACID están ahora disponibles en Couchbase. Ver: Transacciones ACID para aplicaciones NoSQL para más información

Las transacciones multidocumento no se trataron en la entrada anterior de esta serie: Propiedades ACID y Couchbase. Esa entrada de blog cubría los bloques de construcción de ACID que Couchbase soporta para el solo documento. En esta entrada del blog, vamos a utilizar esa base para construir algo como una transacción atómica y distribuida de múltiples documentos.

Descargo de responsabilidad: el código de esta entrada de blog no se recomienda para producción. Es un ejemplo simplificado que puede te será útil tal y como está, pero necesitará endurecerse y pulirse antes de estar listo para producción. La intención es darte una idea de lo que se necesitaría para esas (esperemos que raras) situaciones en las que necesitas transacciones multi-documento con Couchbase.

Breve resumen

En la parte 1, vimos que las propiedades ACID están realmente disponibles en Couchbase a nivel de documento único. Para casos de uso donde los documentos pueden almacenar datos juntos de forma desnormalizada, esto es adecuado. En algunos casos, la desnormalización en un único documento no es suficiente para cumplir los requisitos. Para ese pequeño número de casos de uso, puede que quieras considerar el ejemplo de esta entrada de blog.

Una nota de advertencia: esta entrada del blog es una Inicio para ti. Tu caso de uso, tus necesidades técnicas, las capacidades de Couchbase Server y los casos extremos que te preocupan variarán. A día de hoy no existe un enfoque único.

Ejemplo de transacciones multidocumento

Vamos a centrarnos en una operación sencilla para mantener el código simple. Para casos más avanzados, puedes basarte en este código y posiblemente generizarlo y adaptarlo como mejor te parezca.

Supongamos que estamos trabajando en un juego. Este juego consiste en crear y gestionar granjas (parece una locura, lo sé). Supongamos que en este juego tienes un granero que contiene un cierto número de gallinas. Tu amigo también tiene un granero con un número determinado de gallinas. En algún momento, puede que quieras transferir algunas gallinas de tu granero al granero de tu amigo.

En este caso, la normalización de datos probablemente no ayudará. Porque:

  • Un único documento que contenga todos los graneros no va a funcionar para un juego de un tamaño significativo.
  • No tiene sentido que tu documento del granero contenga el documento del granero de tu amigo (o viceversa).
  • El resto de la lógica del juego funciona bien con la atomicidad de un solo documento: sólo la transferencia de pollos es complicada.

Para empezar, todo lo que tenemos son dos documentos "granero" (Granero Grant y Granero Miller):

Initial barn documents

Este método que utilizaremos para transferir pollos se denomina "confirmación en dos fases". Hay seis pasos en total. El código fuente completo está disponible en GitHub.

Después de hacer todas las capturas de pantalla y escribir los ejemplos de código, se me ocurrió que las gallinas viven en gallineros, no en graneros... Pero sígueme la corriente.

0) Documento de transacción

El primer paso es crear un documento de transacción. Se trata de un documento que llevará la cuenta de la transacción multidocumento y de los estado de la transacción. He creado un C# Enum con los posibles estados utilizados en la transacción. Esto será un número cuando se almacene en Couchbase, pero podrías usar cadenas o alguna otra representación si lo deseas.

Comenzará en un estado "Inicial". Al iniciar esta transacción, tenemos un establo "de origen", un establo "de destino" y un número determinado de pollos para transferir.

Volvamos a echar un vistazo a los datos. Ahora hay tres documentos. La transacción es nueva; los documentos del granero son los mismos que al principio.

Initial multi-document transactions document

1) Cambiar a pendiente

A continuación, pongamos el documento de la transacción en estado "pendiente". Más adelante veremos por qué es importante el "estado" de una transacción.

He hecho un poco de trampa aquí, porque estoy usando un ActualizarConCas función. Voy a hacer esto muchas veces, porque actualizar un documento usando una operación Cas puede ser un poco verboso en .NET. Así que he creado una pequeña función de ayuda:

Es un método de ayuda importante. Utiliza bloqueo optimista para actualizar un documento, pero no realiza reintentos ni gestiona errores.

Volvamos a los datos., Seguimos teniendo tres documentos, pero el documento de transacción "estado" se ha actualizado.

Multi-document Transaction pending

2) Modificar los documentos

A continuación, realizaremos las mutaciones necesarias en los documentos del establo. Restaremos un pollo del establo de origen y añadiremos un pollo al establo de destino. Al mismo tiempo, vamos a "etiquetar" estos documentos del establo con el ID del documento de transacción. De nuevo, verá por qué esto es importante más adelante. También almacenaré los valores Cas de estas mutaciones, porque serán necesarios más adelante cuando se realicen más cambios en estos documentos.

En este punto, el código ha movido un pollo entre graneros. Fíjate también en la "etiqueta" de transacción de los graneros.

Barns tagged with transaction

3) Cambiar a comprometido

Hasta aquí, todo bien. Las mutaciones se han completado; es hora de marcar la transacción como "comprometida".

Lo único que ha cambiado es el "estado" de la transacción.

Transaction committed

4) Eliminar etiquetas de transacción

Ahora que la transacción multidocumento está en estado "comprometido", los graneros ya no necesitan saber que forman parte de una transacción. Elimina esas "etiquetas" de los graneros.

Ahora los graneros están libres de la transacción.

Multi-document Transactions tags removed

5) Transacción realizada

El último paso es cambiar el estado de la transacción a "hecho".

Si hemos llegado hasta aquí, entonces la transacción multidocumento está completa. Los graneros tienen el número correcto de pollos después de la transferencia.

Transaction done

Retroceso: ¿qué pasa si algo sale mal?

Es perfectamente posible que algo vaya mal durante las transacciones con varios documentos. Ese es el objetivo de una transacción, en realidad. Todas las operaciones ocurren, o no ocurren.

He puesto el código de los pasos 1 a 5 anteriores dentro de un único bloque try/catch. Una excepción podría ocurrir en cualquier lugar a lo largo del camino, pero vamos a centrarnos en dos puntos críticos.

Excepción durante "pendiente - ¿Cómo debemos actuar si se produce un error justo en medio del paso 2? Es decir, DESPUÉS de que se sustraiga un pollo del granero de origen pero ANTES de que se añada un pollo al granero de destino. Si no manejamos esta situación, un pollo desaparecería en el aire y nuestros jugadores se pondrían a llorar.

Excepción después de "confirmar" la transacción - La transacción tiene un estado de "committed", pero se produce un error antes de que las etiquetas de transacción dejen de estar en los graneros. Si no manejamos esto, entonces podría parecer desde otros procesos que los graneros están todavía dentro de una transacción. El sitio primero la transferencia de pollos tendría éxito, pero no se podrían transferir más pollos.

El código puede manejar estos problemas dentro del captura bloque. Aquí es donde entra en juego el "estado" de la transacción (así como las "etiquetas" de la transacción).

Excepción durante "pendiente

Esta es la situación que haría perder pollos y enfadar a nuestros jugadores. El objetivo es reponer los pollos perdidos y devolver los graneros al estado en que estaban antes de la transacción.

Supongamos que ocurre justo en el medio. Para este ejemplo, tenemos una nueva transacción: transferir 1 pollo del granero Burrows (12 pollos) al granero White (13 pollos).

Barns before rollback

Se ha producido un error justo en medio. El granero de origen tiene un pollo menos, pero el de destino no lo ha recibido.

Barns inconsistent and transaction

He aquí los 3 pasos para la recuperación:

1) Cancelar la transacción

Cambia el estado de la transacción a "cancelando". Más tarde la cambiaremos a "cancelada".

Lo único que ha cambiado hasta ahora es el documento de transacción:

Transaction now cancelling

2) Revertir los cambios

A continuación, tenemos que revertir el estado de los graneros a lo que eran antes. Tenga en cuenta que esto SÓLO es necesario si el granero tiene una etiqueta de transacción. Si no tiene etiqueta, sabemos que ya está en su estado anterior a la transacción. Si tiene una etiqueta, elimínela.

Ahora los graneros vuelven a ser lo que eran antes.

Barns rolled back

3) Transacción cancelada

Lo último que hay que hacer es establecer la transacción como "cancelada".

Y ahora, la transacción está "cancelada".

Transaction cancelled

Esto preserva el número total de pollos en el juego. En este punto, usted todavía necesita manejar el error que causó la necesidad de un retroceso. Puede reintentar, notificar a los jugadores, registrar un error, o todo lo anterior.

Excepción durante "comprometido"

A continuación, veamos otro caso: los cambios en los graneros se han completado, pero aún no se han eliminado sus etiquetas de transacción. Suponiendo que la lógica del juego se preocupe por estas etiquetas, las futuras transacciones multidocumento podrían no ser posibles.

La misma lógica de reversión maneja esta situación también.

Problemas y casos extremos

Este ejemplo simplificado puede ser perfecto para su aplicación, pero hay muchos casos extremos en los que pensar.

¿Y si el proceso muere a mitad de camino? Esto significa que el código ni siquiera llega al captura bloqueo. Es posible que tenga que comprobar si hay transacciones multidocumento incompletas al iniciar la aplicación y realizar la recuperación allí. O posiblemente tener un proceso de vigilancia diferente que busque transacciones multi-documento incompletas.

¿Y si hay una lectura durante la transacción? Supongamos que "cojo" los graneros justo entre sus actualizaciones. Esto será una lectura "sucia", que puede ser problemática.

¿En qué estado ha quedado todo? ¿De quién es la responsabilidad de completar/revertir las transacciones pendientes de varios documentos?

¿Qué ocurre si el mismo documento forma parte de dos transacciones con varios documentos a la vez? Tendrás que incorporar una lógica para evitar que esto ocurra.

La muestra contiene todo el estado para retroceder. Pero si desea más tipos de transacciones (tal vez desea transferir vacas)? Necesitaría un identificador de tipo de transacción, o bien generizar el código de transacción para poder abstraer el "importe" utilizado en los ejemplos anteriores y especificar en su lugar la versión actualizada del documento.

Otros casos extremos. ¿Qué ocurre si hay un nodo en tu cluster que falla en mitad de la transacción? ¿Qué ocurre si no consigues los bloqueos que quieres? ¿Cuánto tiempo hay que reintentarlo? ¿Cómo se identifica una transacción fallida (timeouts)? Hay montones y montones de casos extremos con los que lidiar. Deberías probar a fondo todas las condiciones que esperas encontrar en producción. Y al final, es posible que desee considerar algún tipo de estrategia de mitigación. Si detectas un problema o encuentras un fallo, puedes dar algunas gallinas gratis a todas las partes implicadas después de arreglar el fallo.

Otras opciones

Nuestro equipo de ingenieros ha estado experimentando con Transacciones RAMP del lado del cliente. RAMP (Read Atomic Multi-Partition) es una forma de garantizar la visibilidad atómica en bases de datos distribuidas. Para más información, consulte RAMPA Fácil de Jon Haddad o Visibilidad atómica escalable con transacciones RAMP por Peter Bailis.

El ejemplo más maduro elaborado para Couchbase es el de Graham Pople utiliza el SDK de Java. Tampoco se trata de una biblioteca lista para producción. Pero, Graham está haciendo algunas cosas interesantes con transacciones multi-documento del lado del cliente. Permanece atento.

Otra opción es el código abierto Biblioteca NDescribe de Iain Cartledge (que es Campeón de la comunidad Couchbase).

Por último, consulte el Patrón Sagaque es especialmente útil para las transacciones multidocumento entre microservicios.

Conclusión

Esta entrada del blog hablaba de cómo usar las primitivas ACID disponibles en Couchbase para crear una especie de transacción atómica multi-documento para una base de datos distribuida. Esto todavía no es un reemplazo completamente sólido para ACID, pero es suficiente para lo que la gran mayoría de las aplicaciones modernas basadas en microservicios necesitan. Para el pequeño porcentaje de casos de uso que necesitan garantías transaccionales adicionales, Couchbase continuará innovando aún más.

Gracias a Mike Goldsmith, Graham Pople y Shivani Gupta, que ayudaron a revisar esta entrada del blog.

Si estás deseando aprovechar las ventajas de una base de datos distribuida como Couchbase, pero aún te preocupan las transacciones multidocumento, ¡ponte en contacto con nosotros! Puedes hacernos preguntas en el Foros de Couchbase o puede ponerse en contacto conmigo en Twitter @mgroves.

Comparte este artículo
Recibe actualizaciones del blog de Couchbase en tu bandeja de entrada
Este campo es obligatorio.

Autor

Publicado por Matthew Groves

Matthew D. Groves es un apasionado de la programación. No importa si se trata de C#, jQuery o PHP: envía solicitudes de incorporación de cambios para cualquier cosa. Se dedica profesionalmente a la programación desde que escribió una aplicación QuickBASIC para el punto de venta de la pizzería de sus padres en los años 90. Actualmente trabaja como gerente sénior de marketing de productos para Couchbase. Dedica su tiempo libre a su familia, a ver los partidos de los Reds y a participar en la comunidad de desarrolladores. Es autor de AOP in .NET, Pro Microservices in .NET, autor de Pluralsight y MVP de Microsoft.

1 Comentarios

  1. Uno de los puntos fuertes de los Microservicios es enviar eventos durante las escrituras, a un topic y luego hacer que otros como las vistas consuman el evento desde el topic. Para asegurarse de que el evento se envía al tema, una solución simple y eficaz es escribir el evento en la base de datos (digamos en una tabla llamada EventsToBeSent) junto con la raíz agregada, por ejemplo, y tener un trabajador que sondea esta tabla y empuja los eventos (en orden) al tema (Kafka, por ejemplo).
    Usted entiende inmediatamente que, es casi imposible almacenar los eventos en el AR por lo que necesitan ser almacenados solo. Para lograr esto, con Couchbase, fuera de la caja parece ser imposible, y el uso de su enfoque parece ser extremadamente bajo.
    Otra posible solución es almacenar el evento dentro del documento AR, y hacer que el poller sondee todos los ARs con SELECT flat_array(ar.events) FROM bucket ar where type in ("AR1", "AR2", "AR3") WHERE count(ar.events) > 0 sort by ar.timeStamp. Pero hay tres problemas principales con este enfoque: 1) Las consultas son inconsistentes en Couchbase (a menos que utilices un truco para la consistencia fuerte), 2) El rendimiento de esta consulta en sí en comparación con el viejo y probado RDBMS, 3) El rendimiento de la eliminación de los eventos de los ARs. En un RDBMS sólo tienes que hacer un simple UPDATE EventsToBeSent e SET e.IsSent = true WHERE e.Id in (,,,).

Deja un comentario

¿Listo para empezar con Couchbase Capella?

Empezar a construir

Consulte nuestro portal para desarrolladores para explorar NoSQL, buscar recursos y empezar con tutoriales.

Utilizar Capella gratis

Ponte manos a la obra con Couchbase en unos pocos clics. Capella DBaaS es la forma más fácil y rápida de empezar.

Póngase en contacto

¿Quieres saber más sobre las ofertas de Couchbase? Permítanos ayudarle.