¿Puedo hacer transacciones y bloqueos en CouchDB?

81

Necesito hacer transacciones (comenzar, confirmar o deshacer), bloqueos (seleccionar para actualizar). ¿Cómo puedo hacerlo en un modelo de documento db?

Editar:

El caso es este:

  • Quiero ejecutar un sitio de subastas.
  • Y también pienso en cómo realizar compras directas.
  • 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. Por eso necesito candados y transacciones.
  • No sé cómo abordar eso sin bloqueos y / o transacciones.

¿Puedo resolver esto con CouchDB?

user2427
fuente

Respuestas:

145

No. CouchDB utiliza 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 actual del documento no coincide con lo que envió.

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

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 simultaneidad como este:

  1. Recupere el documento, tome nota de la _revpropiedad que envía CouchDB
  2. Disminuir el campo de cantidad, si es mayor que cero
  3. Devuelva el documento actualizado, utilizando la _revpropiedad
  4. Si _revcoincide con el número almacenado actualmente, ¡listo!
  5. Si hay un conflicto (cuando _revno 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, lo maneja como lo haría en un RDBMS y alerta al usuario de que en realidad no puede comprar lo que quería comprar. Si la versión más reciente del documento tiene una cantidad mayor que 0, simplemente repita la operación con los datos actualizados y comience desde el principio. Esto le obliga a hacer un poco más de trabajo que lo que haría un RDBMS, y podría resultar un poco molesto si hay actualizaciones frecuentes y conflictivas.

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

Comenzaría con un documento de "producto maestro" 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_keyy claimed_by. Si usted está vendiendo un modelo de martillo, y tienen 20 de ellos para vender, es posible que tenga documentos con teclas como hammer-1, hammer-2, etc., para representar cada martillo disponibles.

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". Estos están completamente fuera de lugar, pero deberían darle una idea de cómo se verí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 idy _rev) hasta que reclame con éxito uno (los boletos reclamados anteriormente 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 el número total de inventory_ticketartículos 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 en conflicto y reduce la necesidad de responder a un conflicto con una nueva actualización. Bajo este modelo, no tendrá varios usuarios intentando cambiar datos en la entrada del producto principal. En el peor de los casos, tendrá varios usuarios intentando reclamar un solo boleto, y si ha tomado varios de ellos desde su vista, simplemente pase al siguiente boleto e intente nuevamente.

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

MrKurt
fuente
4
No me queda claro cómo tener 'tickets' que intentas reclamar en secuencia es una mejora significativa sobre simplemente volver a intentar la lectura / modificación / escritura para actualizar la entidad maestra. Ciertamente, no parece que valga la pena los gastos generales adicionales, especialmente si tiene grandes cantidades de stock.
Nick Johnson
4
Desde mi perspectiva, la convención de boletos es "más simple" de construir. Las actualizaciones fallidas en la entrada maestra requieren que vuelva a cargar el documento, realice la operación nuevamente y luego guarde. La cosa del ticket te permite intentar "reclamar" algo sin tener que solicitar más datos.
MrKurt
Además, depende de qué tipo de gastos generales le preocupe. O lucharás con una mayor contención o tendrás requisitos de almacenamiento adicionales. Dado que un boleto también puede funcionar como un registro de compra, no sé si habría tantos problemas de almacenamiento como crees.
MrKurt
2
Estoy editando un campo de cantidad de un documento de producto. Entonces debo crear miles de "boletos" si la cantidad = 2K, por ejemplo. Luego reduzco una cantidad, debo eliminar algunas entradas. Suena completamente relajado para mí. Mucho dolor de cabeza en casos de uso básicos. Tal vez me esté perdiendo algo, pero ¿por qué no recuperar el comportamiento de transacción eliminado anteriormente, simplemente hacerlo opcional con algo como _bulk_docs? Verify_on_conflict = true. Muy útil en configuraciones de un solo maestro.
Sam
3
@mehaase: Lea esto: guide.couchdb.org/draft/recipes.html , la respuesta se reduce a la estructura de datos interna de couchdb "nunca cambia los datos, solo agrega nuevos". En su escenario, eso significa crear una transacción (atómica) desde la cuenta a una cuenta en tránsito para el débito y una segunda transacción (atómica) desde la cuenta en tránsito hacia adelante (o hacia atrás). Así es como lo hacen los bancos reales. Cada paso está siempre documentado.
Fabian Zeindl
26

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 boleto, puede seleccionar al azar entre los boletos restantes. Dada una gran cantidad de tickets y una gran cantidad de solicitudes simultáneas, obtendrá una contención mucho menor en esos tickets, en comparación con todos los que intentan obtener el primer ticket.

Kerr
fuente
21

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

  • Cree un documento de transacción "transferir USD 10 de la cuenta 11223 a la cuenta 88733". Esto crea tensión en el sistema.
  • Para resolver cualquier escaneo de tensión para todos los documentos de transacciones y
    • Si la cuenta de origen aún no está actualizada, 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 (p. Ej., Establezca la marca "de origen" en el documento)
    • Si la cuenta de destino aún no está actualizada, actualice la cuenta de destino (+10 USD)
    • Si la cuenta de destino se actualizó pero el documento de transacción no muestra esto, actualice el documento de transacción
    • Si ambas cuentas se han actualizado, puede eliminar el documento de la transacción o conservarlo para auditoría.

El escaneo de tensión debe realizarse en un proceso de backend para todos los "documentos de tensión" para mantener cortos los tiempos de tensión en el sistema. En el ejemplo anterior, se anticipará una incoherencia breve cuando la primera cuenta se haya actualizado pero la segunda aún no se haya actualizado. Esto debe tenerse en cuenta de la misma manera que tratará la coherencia eventual si se distribuye su Couchdb.

Otra posible implementación 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 suma en los documentos de transacción en los que esta cuenta está involucrada. En Couchdb puede modelar esto muy bien como una vista de mapa / reducción.

ordnungswidrig
fuente
5
Pero, ¿qué pasa con los casos en los que se carga la cuenta pero no se cambia el documento de tensión? Cualquier escenario de falla entre esos dos puntos, si no son atómicos, causará una inconsistencia permanente, ¿verdad? Algo sobre el proceso tiene que ser atómico, ese es el objetivo de una transacción.
Ian Varley
Sí, tiene razón, en este caso, mientras la tensión no se resuelva, habrá inconsistencia. Sin embargo, la inconsistencia es solo temporal hasta que el siguiente escaneo de documentos de tensión lo detecta. Ese es el comercio en este caso, una especie de consistencia eventual con respecto al tiempo. Siempre que disminuya la cuenta de origen primero y luego incremente la cuenta de destino, esto puede ser aceptable. Pero cuidado: los documentos de tensión no le proporcionarán transacciones ACID además de REST. Pero pueden ser una buena compensación entre REST puro y ACID.
ordnungswidrig
4
Imagine que cada documento de tensión tiene una marca de tiempo y los documentos de cuenta tienen un campo de "última tensión aplicada", o una lista de tensiones aplicadas. Cuando debita la cuenta de origen, también actualiza el campo 'última tensión aplicada'. Esas dos operaciones son atómicas porque están en el mismo documento. La cuenta de destino también tiene un campo similar. De esa manera, el sistema siempre puede decir qué documentos de tensión se han aplicado a qué cuentas.
Jesse Hallett
1
¿Cómo detectar si el documento de origen / destino ya se actualizó? ¿Qué pasa si falla después del paso 1, luego se vuelve a ejecutar y falla nuevamente, y así sucesivamente, seguirá deduciendo la cuenta de origen?
wump
1
@wump: deberá registrar que el documento de tensión se ha aplicado a la cuenta. por ejemplo, agregando la identificación del documento de tensión en una propiedad de lista de cualquier cuenta. cuando todas las cuentas tocadas por el documento de tensión se hayan actualizado, marque el documento de tensión como "hecho" o elimínelo. Posteriormente, la identificación del documento se puede eliminar de la lista para todas las cuentas.
ordnungswidrig
6

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 modifican un documento en particular se conectan a la misma instancia de CouchDB, entonces puede usar el sistema de detección de conflictos para crear una especie de atomicidad usando los métodos descritos anteriormente, pero si luego escala a un clúster. o utilice un servicio alojado como Cloudant, se estropeará y tendrá que rehacer esa parte del sistema.

Entonces, mi sugerencia sería usar algo diferente a CouchDB para los saldos de su cuenta, será mucho más fácil de esa manera.

Dobes Vandermeer
fuente
5

Como respuesta al problema del OP, Couch probablemente no sea la mejor opción aquí. El uso de vistas es una excelente manera de realizar un seguimiento del inventario, pero limitarlo 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 "hammer-1" y luego escribe un documento para usarlo. El problema es que no hay una forma atómica de escribir solo el documento para usar el martillo si el resultado de la vista es que hay> 0 martillos-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 su base de datos está ejecutando localhost. Pero una vez que escala y tiene un servidor de base de datos o un clúster externo, el problema se hará mucho más notorio.

Una actualización de la respuesta de MrKurt (puede que solo esté fechada o que no conozca algunas de las características de CouchDB)

Una vista es una buena manera de manejar cosas como saldos / inventarios en CouchDB.

No es necesario emitir el 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 aumente innecesariamente.

Una vista simple para rastrear los saldos de inventario debería verse más como esta (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

Esto usa una función de reducción incorporada que solo suma los valores de todas las filas con claves coincidentes.

En esta vista, cualquier documento puede tener un miembro "InventoryChange" que asigna product_key's 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's y 25 saw_4321's.

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

Quemaría 5 martillos del inventario.

Con este modelo, nunca actualiza ningún dato, solo agrega. Esto significa que no hay posibilidad de conflictos de actualización. Todos los problemas transaccionales de actualizar datos desaparecen :)

Otra cosa buena de este modelo es que CUALQUIER documento en la base de datos puede sumar y restar artículos del inventario. Estos documentos pueden contener todo tipo de datos. Es posible que tenga un documento de "Envío" con un montón de datos sobre la fecha y la hora de recepción, el almacén, el empleado de recepción, etc. y siempre que ese documento defina un Cambio de inventario, actualizará el inventario. Al igual que un documento de "Venta" y un documento de "DamagedItem", etc. Al mirar cada documento, se leen muy claramente. Y la vista maneja todo el trabajo duro.

wallacer
fuente
Interesante estrategia. Como novato de CouchDB, parecería que para calcular el número actual de martillos, debe realizar un mapa / reducción en todo el historial de cambios de inventario de martillos de la empresa. Esto podría significar años de cambios. ¿Hay alguna característica incorporada de CouchDB que lo haga efectivo?
chadrik
Sí, las vistas en CouchDB son como un mapa / reducción continuo y persistente. Tiene razón en que hacerlo desde cero en un gran conjunto de datos llevaría años, pero cuando se agregan nuevos documentos, solo actualizan la vista existente, no tiene que volver a calcular la vista completa. Tenga en cuenta que existe un requisito de espacio y CPU para las vistas. Además, al menos cuando trabajé con CouchDB profesionalmente (han pasado algunos años), era muy importante usar solo las funciones de reducción integradas, es decir. _suma. Las funciones personalizadas de reducción de Javascript eran extremadamente lentas
wallacer
3

De hecho, puedes de alguna manera. Eche un vistazo a la API de documentos HTTP y desplácese hacia abajo hasta el título "Modificar varios documentos con una sola solicitud".

Básicamente, puede crear / actualizar / eliminar un montón de documentos en una sola solicitud de publicación a URI / {dbname} / _ bulk_docs y todos tendrán éxito o 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.

Evan
fuente
Eso realmente no ayudaría en la situación que se está discutiendo, es decir, la contención de documentos únicos de varios usuarios.
Kerr
3
A partir de CouchDB 0.9, la semántica de las actualizaciones masivas ha cambiado.
Barry Wark
0

Simplemente use el tipo de solución liviana SQlite para transacciones, y cuando la transacción se complete con éxito, repítela y márquela como replicada en SQLite

Tabla SQLite

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

También puede eliminar las transacciones que se replican correctamente.

Ravinder Payal
fuente