¿Cómo solucionar la falta de transacciones en MongoDB?

Sé que hay preguntas similares aquí, pero me están diciendo que vuelva a los sistemas RDBMS normales si necesito transacciones o uso operaciones atómicas o confirmación en dos fases . La segunda solución parece la mejor opción. El tercero no deseo seguirlo porque parece que muchas cosas podrían salir mal y no puedo probarlo en todos los aspectos. Me está costando refactorizar mi proyecto para realizar operaciones atómicas. No sé si esto proviene de mi punto de vista limitado (hasta ahora solo he trabajado con bases de datos SQL), o si realmente no se puede hacer.

Nos gustaría probar piloteando MongoDB en nuestra compañía. Hemos elegido un proyecto relativamente simple: un portal SMS. Permite que nuestro software envíe mensajes SMS a la red celular y la puerta de enlace hace el trabajo sucio: de hecho se comunica con los proveedores a través de diferentes protocolos de comunicación. La puerta de enlace también administra la facturación de los mensajes. Cada cliente que solicita el servicio tiene que comprar algunos créditos. El sistema disminuye automáticamente el saldo del usuario cuando se envía un mensaje y niega el acceso si el saldo es insuficiente. Además, debido a que somos clientes de proveedores de SMS de terceros, también podemos tener nuestros propios saldos con ellos. Tenemos que hacer un seguimiento de esos también.

Empecé a pensar en cómo puedo almacenar los datos requeridos con MongoDB si reduzco cierta complejidad (facturación externa, envío de SMS en cola). Procedente del mundo SQL, crearía una tabla separada para los usuarios, otra para los mensajes SMS y otra para almacenar las transacciones relacionadas con el saldo de los usuarios. Digamos que creo colecciones separadas para todos aquellos en MongoDB.

Imagine una tarea de envío de SMS con los siguientes pasos en este sistema simplificado:

  1. verificar si el usuario tiene saldo suficiente; negar el acceso si no hay suficiente crédito

  2. envíe y almacene el mensaje en la colección de SMS con los detalles y el costo (en el sistema en vivo, el mensaje tendría un atributo de status y una tarea lo recogería para la entrega y establecería el precio del SMS según su estado actual)

  3. disminuir el saldo de los usuarios por el costo del mensaje enviado

  4. registrar la transacción en la colección de transacciones

Ahora, ¿cuál es el problema con eso? MongoDB puede hacer actualizaciones atómicas solo en un documento. En el flujo anterior, podría suceder que se produzca algún tipo de error y el mensaje se guarde en la base de datos, pero el saldo del usuario no se actualiza y / o la transacción no se registra.

Se me ocurrieron dos ideas:

  • Cree una única colección para los usuarios y almacene el saldo como un campo, las transacciones relacionadas con el usuario y los mensajes como documentos secundarios en el documento del usuario. Debido a que podemos actualizar los documentos atómicamente, esto resuelve el problema de la transacción. Desventajas: si el usuario envía muchos mensajes SMS, el tamaño del documento podría llegar a ser grande y se podría alcanzar el límite del documento de 4MB. Tal vez pueda crear documentos de historia en tales escenarios, pero no creo que sea una buena idea. Además, no sé qué tan rápido sería el sistema si inserto más y más datos en el mismo documento grande.

  • Crea una colección para usuarios y otra para transacciones. Puede haber dos tipos de transacciones: compra de crédito con cambio de saldo positivo y mensajes enviados con cambio de saldo negativo. La transacción puede tener un subdocumento; por ejemplo, en los mensajes enviados, los detalles del SMS se pueden incrustar en la transacción. Desventajas: no almaceno el saldo actual del usuario, así que tengo que calcularlo cada vez que un usuario intenta enviar un mensaje para decir si el mensaje puede pasar o no. Me temo que este cálculo puede volverse lento a medida que crece la cantidad de transacciones almacenadas.

Estoy un poco confundido acerca de qué método elegir. ¿Hay otras soluciones? No pude encontrar las mejores prácticas en línea sobre cómo solucionar este tipo de problemas. Supongo que muchos progtwigdores que están tratando de familiarizarse con el mundo NoSQL enfrentan problemas similares al principio.

Mira esto , por Tokutek. Desarrollan un complemento para Mongo que promete no solo transacciones sino también un aumento en el rendimiento.

Vivir sin transacciones

Las transacciones son compatibles con las propiedades de ACID , pero aunque no hay transacciones en MongoDB , sí tenemos operaciones atómicas. Bueno, las operaciones atómicas significan que cuando trabajas en un solo documento, ese trabajo se completará antes de que nadie más vea el documento. Verán todos los cambios que hicimos o ninguno de ellos. Y al usar operaciones atómicas, a menudo puedes lograr lo mismo que hubiéramos logrado usando transacciones en una base de datos relacional. Y la razón es que, en una base de datos relacional, necesitamos hacer cambios en múltiples tablas. Por lo general, las tablas que deben unirse, por lo que queremos hacer todo de una vez. Y para hacerlo, dado que hay varias tablas, tendremos que comenzar una transacción y hacer todas esas actualizaciones y luego finalizar la transacción. Pero con MongoDB , vamos a incrustar los datos, ya que los vamos a unir previamente en documentos y son estos documentos ricos que tienen jerarquía. A menudo podemos lograr lo mismo. Por ejemplo, en el ejemplo del blog, si queríamos asegurarnos de que actualizamos una publicación de blog de manera atómica, podemos hacerlo porque podemos actualizar la publicación completa del blog a la vez. Donde, como si se tratara de un conjunto de tablas relacionales, probablemente tendremos que abrir una transacción para que podamos actualizar la recostackción de publicaciones y la recostackción de comentarios.

Entonces, ¿cuáles son nuestros enfoques que podemos tomar en MongoDB para superar la falta de transacciones?

  • reestructurar : reestructurar el código, de modo que trabajemos en un solo documento y aprovechemos las operaciones atómicas que ofrecemos dentro de ese documento. Y si hacemos eso, entonces generalmente estamos listos.
  • implementar en software : podemos implementar el locking en el software, al crear una sección crítica. Podemos construir una prueba, prueba y configuración usando find y modify. Podemos construir semáforos, si es necesario. Y de alguna manera, esa es la forma en que el mundo más grande funciona de todos modos. Si lo pensamos, si un banco necesita transferir dinero a otro banco, no está viviendo en el mismo sistema relacional. Y cada uno tiene sus propias bases de datos relacionales a menudo. Y deben poder coordinar esa operación aunque no podamos comenzar la transacción y finalizar la transacción en esos sistemas de bases de datos, solo dentro de un sistema dentro de un banco. Por lo tanto, hay formas de software para solucionar el problema.
  • tolerar : el enfoque final, que a menudo funciona en aplicaciones web modernas y otras aplicaciones que requieren una gran cantidad de datos, es simplemente tolerar un poco de inconsistencia. Un ejemplo sería, si estamos hablando de un feed de un amigo en Facebook, no importa si todos vean su actualización de pared al mismo tiempo. Si okey, si una persona es baja unos pocos golpes por unos segundos y se ponen al día. A menudo no es crítico en muchos diseños de sistemas que todo se mantenga perfectamente consistente y que todos tengan una vista de la base de datos perfectamente consistente y la misma. Entonces, podríamos simplemente tolerar un poco de inconsistencia que es algo temporal.

Update findAndModify Update , findAndModify , $addToSet (dentro de una actualización) y $push (dentro de una actualización) operan atómicamente dentro de un único documento.

A partir de 4.0, MongoDB tendrá transacciones ACID de múltiples documentos. El plan es habilitar primero a aquellos en implementaciones de conjuntos de réplicas, seguidos por los clústeres fragmentados. Las transacciones en MongoDB se sentirán como las transacciones con las que los desarrolladores están familiarizados a partir de las bases de datos relacionales; serán declaraciones múltiples, con semántica y syntax similares (como start_transaction y commit_transaction ). Es importante destacar que los cambios en MongoDB que permiten las transacciones no afectan el rendimiento de las cargas de trabajo que no lo requieren.

Para más detalles, mira aquí .

Traerlo al grano: si la integridad transaccional es imprescindible, entonces no use MongoDB, sino que use solo componentes en las transacciones de respaldo del sistema. Es extremadamente difícil construir algo sobre el componente para proporcionar una funcionalidad similar a ACID para componentes que no son compatibles con ACID. Dependiendo de las cajas de uso individuales, puede tener sentido separar las acciones en acciones transaccionales y no transaccionales de alguna manera …

Ahora, ¿cuál es el problema con eso? MongoDB puede hacer actualizaciones atómicas solo en un documento. En el flujo anterior, podría suceder que se produzca algún tipo de error y el mensaje se guarde en la base de datos, pero el saldo del usuario no se reduce y / o la transacción no se registra.

Esto no es realmente un problema. El error que ha mencionado es un error lógico (error) o IO (red, falla de disco). Este tipo de error puede dejar tiendas transaccionales y sin transacción en un estado no consistente. Por ejemplo, si ya ha enviado SMS, pero mientras se produjo el error de almacenamiento de mensajes, no puede revertir el envío de SMS, lo que significa que no se registrará, el saldo del usuario no se reducirá, etc.

El verdadero problema aquí es que el usuario puede aprovechar la condición de carrera y enviar más mensajes de los que permite su saldo. Esto también se aplica a RDBMS, a menos que envíe SMS dentro de la transacción con locking de campo de saldo (lo que sería un gran cuello de botella). Como una posible solución para MongoDB sería utilizar findAndModify primero para reducir el saldo y comprobarlo, si es negativo no permitir el envío y reembolsar la cantidad (incremento atómico). Si es positivo, continúe enviando y, en caso de que falle, reembolse el monto. La recostackción del historial de saldos también se puede mantener para ayudar a corregir / verificar el campo de saldo.

El proyecto es simple, pero debe admitir transacciones para el pago, lo que dificulta todo. Entonces, por ejemplo, un sistema de portal complejo con cientos de colecciones (foro, chat, anuncios, etc.) es, en cierto sentido, más simple, porque si pierde un foro o entrada de chat, a nadie realmente le importa. Si, por otro lado, pierde una transacción de pago que es un problema grave.

Entonces, si realmente quieres un proyecto piloto para MongoDB, elige uno que sea simple a ese respecto.

Las transacciones están ausentes en MongoDB por razones válidas. Esta es una de esas cosas que hacen que MongoDB sea más rápido.

En su caso, si la transacción es obligatoria, mongo no parece una buena opción.

Puede ser RDMBS + MongoDB, pero eso agregará complejidades y dificultará la administración y el soporte de la aplicación.

Este es probablemente el mejor blog que encontré sobre la implementación de transacción como característica para mongodb.!

Indicador de sincronización: mejor para simplemente copiar datos de un documento maestro

Cola de trabajos: muy general, resuelve el 95% de los casos. La mayoría de los sistemas necesitan tener al menos una cola de trabajo de todos modos.

Compromiso de dos fases: esta técnica garantiza que cada entidad siempre tenga toda la información necesaria para llegar a un estado constante

Reconciliación de registros: la técnica más robusta, ideal para sistemas financieros

Versionado: proporciona aislamiento y admite estructuras complejas

Lea esto para obtener más información: https://dzone.com/articles/how-implement-robust-and

Esto es tarde, pero creo que esto ayudará en el futuro. Uso Redis para hacer una cola para resolver este problema.

  • Requisito:
    La imagen a continuación muestra 2 acciones que se deben ejecutar concurrentemente, pero la fase 2 y la fase 3 de la acción 1 deben finalizar antes de comenzar la fase 2 de la acción 2 o al revés (una fase puede ser una solicitud REST api, una solicitud de base de datos o ejecutar código JavaScript …). enter image description here

  • Cómo te ayuda una cola
    En cola, asegúrese de que cada código de bloque entre lock() y release() en muchas funciones no se ejecute al mismo tiempo, haga que se aíslen.

     function action1() { phase1(); queue.lock("action_domain"); phase2(); phase3(); queue.release("action_domain"); } function action2() { phase1(); queue.lock("action_domain"); phase2(); queue.release("action_domain"); } 
  • Cómo construir una cola
    Solo me enfocaré en cómo evitar la parte de la condición de la carrera al construir una cola en el sitio back-end. Si no conoce la idea básica de la cola, ven aquí .
    El siguiente código solo muestra el concepto, necesita implementarlo de la manera correcta.

     function lock() { if(isRunning()) { addIsolateCodeToQueue(); //use callback, delegate, function pointer... depend on your language } else { setStateToRunning(); pickOneAndExecute(); } } function release() { setStateToRelease(); pickOneAndExecute(); } 

Pero necesitas isRunning() setStateToRelease() setStateToRunning() aislarlo o de lo contrario te enfrentas a una condición de carrera nuevamente. Para hacer esto, elijo Redis para fines ACID y escalable.
Documento de Redis habla sobre su transacción:

Todos los comandos en una transacción se serializan y se ejecutan secuencialmente. Nunca puede ocurrir que una solicitud emitida por otro cliente se sirva en el medio de la ejecución de una transacción de Redis. Esto garantiza que los comandos se ejecuten como una única operación aislada.

PD:
Uso Redis porque mi servicio ya lo usa, puede usar cualquier otra forma de aislamiento de soporte para hacer eso.
El action_domain en mi código está arriba para cuando solo necesita la acción 1 llamada por usuario Una acción de locking 2 del usuario A, no bloquea a otro usuario. La idea es poner una clave única para el locking de cada usuario.

Las transacciones están disponibles ahora en MongoDB 4.0. Muestra aquí

 // Runs the txnFunc and retries if TransientTransactionError encountered function runTransactionWithRetry(txnFunc, session) { while (true) { try { txnFunc(session); // performs transaction break; } catch (error) { // If transient error, retry the whole transaction if ( error.hasOwnProperty("errorLabels") && error.errorLabels.includes("TransientTransactionError") ) { print("TransientTransactionError, retrying transaction ..."); continue; } else { throw error; } } } } // Retries commit if UnknownTransactionCommitResult encountered function commitWithRetry(session) { while (true) { try { session.commitTransaction(); // Uses write concern set at transaction start. print("Transaction committed."); break; } catch (error) { // Can retry commit if (error.hasOwnProperty("errorLabels") && error.errorLabels.includes("UnknownTransactionCommitResult") ) { print("UnknownTransactionCommitResult, retrying commit operation ..."); continue; } else { print("Error during commit ..."); throw error; } } } } // Updates two collections in a transactions function updateEmployeeInfo(session) { employeesCollection = session.getDatabase("hr").employees; eventsCollection = session.getDatabase("reporting").events; session.startTransaction( { readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } } ); try{ employeesCollection.updateOne( { employee: 3 }, { $set: { status: "Inactive" } } ); eventsCollection.insertOne( { employee: 3, status: { new: "Inactive", old: "Active" } } ); } catch (error) { print("Caught exception during transaction, aborting."); session.abortTransaction(); throw error; } commitWithRetry(session); } // Start a session. session = db.getMongo().startSession( { mode: "primary" } ); try{ runTransactionWithRetry(updateEmployeeInfo, session); } catch (error) { // Do something with error } finally { session.endSession(); }