我需要执行事务(开始、提交或回滚)和锁定(选择进行更新)操作。在文档模型数据库中,我该如何实现它?
编辑:
情况是这样的:
- 我想运行一个拍卖网站。
- 并考虑如何进行直接购买。
- 在直接购买中,我必须减少项目记录中的数量字段,但仅当数量大于零时才能这样做。这就是为什么我需要锁定和事务的原因。
- 我不知道如何在没有锁定和/或事务的情况下解决这个问题。
我能用CouchDB解决这个问题吗?
不,CouchDB使用“乐观并发”模型。简单来说,这意味着您在更新时发送文档版本,如果当前文档版本与您发送的版本不匹配,则CouchDB将拒绝更改。
实际上,这看起来很简单。您可以为CouchDB重新构建许多正常的基于事务的场景。虽然在学习CouchDB时需要放弃关系数据库领域的知识,但从更高的层次来解决问题会有所帮助,而不是试图将CouchDB适配到基于SQL的世界中。
跟踪库存
你提出的问题主要是一个库存问题。如果你有一个描述物品的文档,并且包括一个“可用数量”的字段,你可以处理这样的并发问题:
_rev
属性_rev
属性发送更新后的文档_rev
匹配当前存储的编号,则完成!_rev
不匹配时),则检索最新的文档版本在这种情况下,有两种可能的失败场景需要考虑。如果最新文档版本的数量为0,则处理方式与在关系数据库中一样,并提示用户他们实际上无法购买所需物品。如果最新文档版本的数量大于0,则只需使用更新后的数据重复操作,并重新开始。这要求您比关系数据库做更多的工作,如果有频繁的冲突更新,可能会有些麻烦。
现在,我刚才给出的答案假定您将在CouchDB中以与关系型数据库类似的方式处理事务。我可能会以不同的方式解决这个问题:
我将从包含所有描述符数据(名称、图片、描述、价格等)的“主产品”文档开始。然后,我会为每个特定实例添加一个“库存票据”文档,其中包括product_key
和claimed_by
字段。如果你正在销售一个锤子型号,并且有20个可供销售,你可能会有像hammer-1
、hammer-2
等键来代表每个可用的锤子。
然后,我会创建一个视图,以获取可用的锤子列表,并具有让我查看“总数”的规约函数。这些完全是临时的,但应该能给您一个工作视图的想法。
Map
function(doc)
{
if (doc.type == 'inventory_ticket' && doc.claimed_by == null ) {
emit(doc.product_key, { 'inventory_ticket' :doc.id, '_rev' : doc._rev });
}
}
这给我一个按产品密钥列出可用“票证”的列表。当有人想购买锤子时,我可以获取其中一组,然后迭代地发送更新(使用id
和_rev
),直到成功声明一个(先前声明的票证将导致更新错误)。
减少
function (keys, values, combine) {
return values.length;
}
这个 reduce 函数简单地返回未声明的 inventory_ticket
项目总数,因此您可以知道有多少“锤子”可供购买。
注意事项:
这种解决方案大约花费了3.5分钟的总思考时间来解决您提出的特定问题。可能有更好的方法!尽管如此,它确实显著减少了冲突更新,并减少了需要用新的更新响应冲突的需求。在这种模式下,您不会有多个用户试图更改主要产品条目中的数据。最坏的情况是,多个用户试图声明单个票证,如果您从视图中获取了几个票证,则只需转移到下一个票证并重试即可。
参考:https://wiki.apache.org/couchdb/Frequently_asked_questions#How_do_I_use_transactions_with_CouchDB.3F
对MrKurt答案的进一步拓展。在许多情况下,您不需要按顺序兑换库存票。而是可以从剩余的票中随机选择,而不是选择第一个票。如果有大量票和大量并发请求,则与每个人都尝试获取第一个票相比,这些票的争用将大大减少。
一种针对RESTful事务的设计模式是在系统中创建"紧张"。对于一个常见的银行账户交易用例,您必须确保更新涉及到的两个账户的总额:
扫描紧张状态应该在后端进程中完成,适用于所有"紧张文件",以保持系统中的紧张时间短暂。在上面的示例中,当第一个帐户已更新但第二个帐户尚未更新时,预计会存在短时间的不一致。如果您的CouchDB是分布式的,就必须考虑这一点,以同样的方式处理最终一致性。
另一种可能的实现方法完全避免了需要使用事务:只需存储紧张文档,并通过评估每个涉及到的紧张文档来评估系统的状态。在上面的例子中,这意味着账户的总额仅由涉及到该账户的交易文档中的值之和确定。在Couchdb中,您可以将其很好地建模为一个映射/减少视图。
function( doc )
{
if( doc.InventoryChange != undefined ) {
for( product_key in doc.InventoryChange ) {
emit( product_key, 1 );
}
}
}
而且reduce函数甚至更加简单
_sum
{
"_id": "abc123",
"InventoryChange": {
"hammer_1234": 10,
"saw_4321": 25
}
}
将添加10个hammer_1234和25个saw_4321。
{
"_id": "def456",
"InventoryChange": {
"hammer_1234": -5
}
}
将从库存中烧掉5个锤子。
使用这种模型,您永远不会更新任何数据,只会进行追加。这意味着没有更新冲突的机会。所有更新数据的事务问题都消失了 :)
这种模型的另一个好处是,数据库中的任何文档都可以向库存添加和减少物品。这些文档可以包含各种其他数据。您可能有一个“装运”文档,其中包含有关收到日期和时间、仓库、接收员工等的大量数据,并且只要该文档定义了InventoryChange,它就会更新库存。销售文件、损坏商品文件等也可以。查看每个文档时,它们都读得非常清楚。视图处理所有繁重的工作。
基本上,您可以在单个post请求中创建/更新/删除一堆文档,URI为 / {dbname} / _bulk_docs ,它们将全部成功或全部失败。但是,该文档警告说这种行为可能会在未来发生改变。
编辑:如预测的那样,在0.9版本中,批量文档不再以这种方式工作。
只需使用轻量级的SQlite解决方案来处理事务,当事务成功完成后进行复制,并在SQLite中标记为已复制
SQLite表
txn_id , txn_attribute1, txn_attribute2,......,txn_status
dhwdhwu$sg1 x y added/replicated
您还可以删除已成功复制的交易。