¿Puedo hacer transacciones y lockings en CouchDB?

Necesito hacer transacciones (comenzar, confirmar o deshacer), lockings (seleccionar para la actualización). ¿Cómo puedo hacerlo en un documento modelo db?

Editar:

El caso es este:

  • Quiero ejecutar un sitio de subastas.
  • Y pienso cómo dirigir la compra también.
  • En una compra directa, tengo que disminuir el campo de cantidad en el registro del artículo, pero solo si la cantidad es mayor que cero. Es por eso que necesito lockings y transacciones.
  • No sé cómo abordar eso sin lockings y / o transacciones.

¿Puedo resolver esto con CouchDB?

No. CouchDB usa un modelo de “concurrencia optimista”. En los términos más simples, esto solo significa que envía una versión del documento junto con su actualización, y CouchDB rechaza el cambio si la versión del documento actual no coincide con lo que ha enviado.

Es engañosamente simple, de verdad. Puede replantear muchos escenarios basados ​​en transacciones normales para CouchDB. Sin embargo, es necesario que elimines el conocimiento de tu dominio RDBMS al aprender CouchDB. Es útil abordar los problemas desde un nivel superior, en lugar de intentar moldear Couch en un mundo basado en SQL.

Hacer un seguimiento del inventario

El problema que describió es principalmente un problema de inventario. Si tiene un documento que describe un artículo, e incluye un campo para “cantidad disponible”, puede manejar problemas de concurrencia como este:

  1. Recupere el documento, tome nota de la propiedad _rev que CouchDB envía a lo largo
  2. Disminuya el campo de cantidad, si es mayor que cero
  3. Enviar de vuelta el documento actualizado, utilizando la propiedad _rev
  4. Si el _rev coincide con el número almacenado actualmente, ¡listo!
  5. Si hay un conflicto (cuando _rev no coincide), recupere la versión más reciente del documento

En este caso, hay dos posibles escenarios de falla en los que pensar. Si la versión más reciente del documento tiene una cantidad de 0, la maneja tal como lo haría en un RDBMS y alerta al usuario de que no puede comprar lo que quiere comprar. Si la versión del documento más reciente tiene una cantidad mayor que 0, simplemente repita la operación con los datos actualizados y comience desde el principio. Esto te obliga a hacer un poco más de trabajo de lo que haría un RDBMS, y podría ser un poco molesto si hay actualizaciones frecuentes y contradictorias.

Ahora, la respuesta que acabo de dar presupone que vas a hacer cosas en CouchDB de la misma manera que lo harías en un RDBMS. Podría abordar este problema de forma un poco diferente:

Comenzaría con un documento de “producto principal” que incluye todos los datos del descriptor (nombre, imagen, descripción, precio, etc.). Luego agregaría un documento de “ticket de inventario” para cada instancia específica, con campos para product_key y claimed_by . Si está vendiendo un modelo de martillo y tiene 20 para vender, es posible que tenga documentos con claves como hammer-1 , hammer-2 , etc., para representar cada martillo disponible.

Luego, crearía una vista que me da una lista de martillos disponibles, con una función de reducción que me permite ver un “total”. Están completamente fuera de lugar, pero deberían darle una idea de cómo sería una vista de trabajo.

Mapa

 function(doc) { if (doc.type == 'inventory_ticket' && doc.claimed_by == null ) { emit(doc.product_key, { 'inventory_ticket' :doc.id, '_rev' : doc._rev }); } } 

Esto me da una lista de “tickets” disponibles, por clave de producto. Podría tomar un grupo de estos cuando alguien quiera comprar un martillo, luego repetir el envío de actualizaciones (usando la id y _rev ) hasta que reclame exitosamente uno (los boletos reclamados previamente darán como resultado un error de actualización).

Reducir

 function (keys, values, combine) { return values.length; } 

Esta función de reducción simplemente devuelve la cantidad total de artículos de ticket de inventory_ticket no reclamados, para que pueda saber cuántos “martillos” están disponibles para su compra.

Advertencias

Esta solución representa aproximadamente 3,5 minutos de pensamiento total para el problema particular que ha presentado. ¡Puede haber mejores formas de hacer esto! Dicho esto, reduce sustancialmente las actualizaciones conflictivas y reduce la necesidad de responder a un conflicto con una nueva actualización. Según este modelo, no tendrá múltiples usuarios que intenten cambiar los datos en la entrada del producto principal. En el peor de los casos, tendrá varios usuarios intentando reclamar un único ticket, y si ha obtenido varios de ellos de su vista, simplemente pase al siguiente ticket y vuelva a intentarlo.

Referencia: https://wiki.apache.org/couchdb/Frequently_asked_questions#How_do_I_use_transactions_with_CouchDB.3F

Ampliando la respuesta de MrKurt. Para muchos escenarios, no es necesario que los boletos de acciones se canjeen en orden. En lugar de seleccionar el primer ticket, puede seleccionar al azar entre los tickets restantes. Dado un gran número de boletos y una gran cantidad de solicitudes concurrentes, obtendrá una contención muy reducida en esas entradas, en comparación con todos los que intenten obtener el primer boleto.

Un patrón de diseño para las transacciones de reposo es crear una “tensión” en el sistema. Para el ejemplo de uso popular de una transacción de cuenta bancaria, debe asegurarse de actualizar el total de ambas cuentas involucradas:

  • Cree un documento de transacción “transferir USD 10 desde la cuenta 11223 a la cuenta 88733”. Esto crea la tensión en el sistema.
  • Para resolver cualquier escaneo de tensión para todos los documentos de transacción y
    • Si la cuenta de origen no se actualiza, actualice la cuenta de origen (-10 USD)
    • Si la cuenta de origen se actualizó pero el documento de la transacción no muestra esto, actualice el documento de la transacción (por ejemplo, configure el indicador “sourcedone” en el documento)
    • Si la cuenta de destino no se actualiza, actualice la cuenta de destino (+10 USD)
    • Si la cuenta de destino se actualizó pero el documento de la transacción no muestra esto, actualice el documento de la transacción.
    • Si ambas cuentas se han actualizado, puede eliminar el documento de la transacción o conservarlo para la auditoría.

El escaneo de tensión debe realizarse en un proceso de back-end para todos los “documentos de tensión” para mantener cortos los tiempos de tensión en el sistema. En el ejemplo anterior, habrá una inconsistencia poco anticipada cuando la primera cuenta se haya actualizado, pero la segunda no se haya actualizado aún. Esto debe tenerse en cuenta de la misma manera que usted tratará con la consistencia eventual si su Couchdb se distribuye.

Otra implementación posible evita la necesidad de transacciones por completo: simplemente almacene los documentos de tensión y evalúe el estado de su sistema evaluando cada documento de tensión involucrado. En el ejemplo anterior, esto significaría que el total de una cuenta solo se determina como los valores de sum en los documentos de transacción donde está involucrada esta cuenta. En Couchdb puedes modelar esto muy bien como una vista de mapa / reducir.

No, CouchDB generalmente no es adecuado para aplicaciones transaccionales porque no admite operaciones atómicas en un entorno agrupado / replicado.

CouchDB sacrificó la capacidad transaccional a favor de la escalabilidad. Para tener operaciones atómicas, necesita un sistema de coordinación central, lo que limita su escalabilidad.

Si puede garantizar que solo tiene una instancia de CouchDB o que todos los que modifiquen un documento en particular se conectan a la misma instancia de CouchDB, entonces podría usar el sistema de detección de conflictos para crear una especie de atomicidad utilizando los métodos descritos anteriormente, pero si luego escala a un clúster o use un servicio alojado como Cloudant, se descompondrá y tendrá que volver a hacer esa parte del sistema.

Entonces, mi sugerencia sería usar algo que no sea CouchDB para los saldos de sus cuentas, será mucho más fácil de esa manera.

Como respuesta al problema del OP, Couch probablemente no sea la mejor opción aquí. El uso de vistas es una gran manera de realizar un seguimiento del inventario, pero el locking a 0 es más o menos imposible. El problema es la condición de carrera cuando lee el resultado de una vista, decide que está bien usar un elemento “martillo-1” y luego escribe un documento para usarlo. El problema es que no existe una forma atómica de escribir solo el documento para usar el martillo si el resultado de la vista es que hay> 0 martillo-1. Si 100 usuarios consultan la vista al mismo tiempo y ven 1 martillo-1, todos pueden escribir un documento para usar un martillo 1, lo que da como resultado -99 martillo-1. En la práctica, la condición de carrera será bastante pequeña, realmente pequeña si tu DB está ejecutando localhost. Pero una vez que escala, y tiene un servidor o clúster DB fuera del sitio, el problema se volverá mucho más notorio. De todos modos, es inaceptable tener una condición de carrera de ese tipo en un sistema crítico relacionado con el dinero.

Una actualización de la respuesta de MrKurt (puede estar fechada, o puede haber ignorado algunas características de CouchDB)

Una vista es una buena forma de manejar cosas como balances / inventarios en CouchDB.

No necesita emitir docid y rev en una vista. Obtienes ambos de forma gratuita cuando recuperas los resultados de la vista. Emitirlos, especialmente en un formato detallado como un diccionario, hará que su vista sea innecesariamente grande.

Una vista simple para rastrear los saldos de inventario debería verse más como esto (también fuera de mi cabeza)

 function( doc ) { if( doc.InventoryChange != undefined ) { for( product_key in doc.InventoryChange ) { emit( product_key, 1 ); } } } 

Y la función de reducción es aún más simple

 _sum 

Utiliza una función incorporada de reducción que solo sum los valores de todas las filas con las teclas correspondientes.

En esta vista, cualquier documento puede tener un miembro “InventoryChange” que asigne product_key a un cambio en el inventario total de ellos. es decir.

 { "_id": "abc123", "InventoryChange": { "hammer_1234": 10, "saw_4321": 25 } } 

Agregaría 10 hammer_1234 y 25 saw_4321.

 { "_id": "def456", "InventoryChange": { "hammer_1234": -5 } } 

Quemaría 5 martillos del inventario.

Con este modelo, nunca actualizas datos, solo anexos. Esto significa que no hay oportunidad para conflictos de actualización. Todos los problemas transaccionales de actualización de datos desaparecen 🙂

Otra cosa agradable sobre este modelo es que CUALQUIER documento en el DB puede agregar y restar elementos del inventario. Estos documentos pueden tener todo tipo de otros datos en ellos. Es posible que tenga un documento de “Envío” con un montón de datos sobre la fecha y la hora recibidas, el almacén, el empleado receptor, etc., y siempre que ese documento defina un Cambio de inventario, actualizará el inventario. Al igual que un documento “Venta” y un documento “DamagedItem”, etc. Al leer cada documento, lo leen con mucha claridad. Y la vista maneja todo el trabajo duro.

En realidad, puedes de alguna manera. Eche un vistazo a la API de documentos HTTP y desplácese hacia abajo hasta el encabezado “Modificar varios documentos con una sola solicitud”.

Básicamente puede crear / actualizar / eliminar un grupo de documentos en una sola solicitud de envío a URI / {dbname} / _ bulk_docs y todos tendrán éxito o todos fallarán. Sin embargo, el documento advierte que este comportamiento puede cambiar en el futuro.

EDITAR: como se predijo, a partir de la versión 0.9 los documentos masivos ya no funcionan de esta manera.

Solo use el tipo de solución liviana de SQlite para transacciones, y cuando la transacción se complete con éxito, replíquela y márquela replicada en SQLite.

Tabla de SQLite

 txn_id , txn_attribute1, txn_attribute2,......,txn_status dhwdhwu$sg1 xy added/replicated 

También puede eliminar las transacciones que se replican con éxito.