我可以在CouchDB中进行事务和锁定吗?

82

我需要执行事务(开始、提交或回滚)和锁定(选择进行更新)操作。在文档模型数据库中,我该如何实现它?

编辑:

情况是这样的:

  • 我想运行一个拍卖网站。
  • 并考虑如何进行直接购买。
  • 在直接购买中,我必须减少项目记录中的数量字段,但仅当数量大于零时才能这样做。这就是为什么我需要锁定和事务的原因。
  • 我不知道如何在没有锁定和/或事务的情况下解决这个问题。

我能用CouchDB解决这个问题吗?

7个回答

150

不,CouchDB使用“乐观并发”模型。简单来说,这意味着您在更新时发送文档版本,如果当前文档版本与您发送的版本不匹配,则CouchDB将拒绝更改。

实际上,这看起来很简单。您可以为CouchDB重新构建许多正常的基于事务的场景。虽然在学习CouchDB时需要放弃关系数据库领域的知识,但从更高的层次来解决问题会有所帮助,而不是试图将CouchDB适配到基于SQL的世界中。

跟踪库存

你提出的问题主要是一个库存问题。如果你有一个描述物品的文档,并且包括一个“可用数量”的字段,你可以处理这样的并发问题:

  1. 检索文档,并注意CouchDB发送的_rev属性
  2. 如果数量字段大于零,则减少数量字段
  3. 使用_rev属性发送更新后的文档
  4. 如果_rev匹配当前存储的编号,则完成!
  5. 如果存在冲突(当_rev不匹配时),则检索最新的文档版本

在这种情况下,有两种可能的失败场景需要考虑。如果最新文档版本的数量为0,则处理方式与在关系数据库中一样,并提示用户他们实际上无法购买所需物品。如果最新文档版本的数量大于0,则只需使用更新后的数据重复操作,并重新开始。这要求您比关系数据库做更多的工作,如果有频繁的冲突更新,可能会有些麻烦。

现在,我刚才给出的答案假定您将在CouchDB中以与关系型数据库类似的方式处理事务。我可能会以不同的方式解决这个问题:

我将从包含所有描述符数据(名称、图片、描述、价格等)的“主产品”文档开始。然后,我会为每个特定实例添加一个“库存票据”文档,其中包括product_keyclaimed_by字段。如果你正在销售一个锤子型号,并且有20个可供销售,你可能会有像hammer-1hammer-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


4
对我来说,持有“票据”并尝试按顺序索取它们相比于简单地重试读/修改/写以更新主实体并没有明显的改进。特别是当你拥有大量库存时,这似乎不值得额外的开销。 - Nick Johnson
5
在我看来,票证约定更“简单”易建立。主要记录更新失败需要重新加载文档,再次执行操作,然后保存。而票证机制允许你尝试“占用”某些内容,无需请求更多数据。 - MrKurt
1
此外,这也取决于你所担心的开销类型。你可能会面临增加争用或额外存储需求的问题。考虑到一张票据也可以兼作购买记录,我不知道存储问题是否像你想象的那样严重。 - MrKurt
2
我正在编辑产品文档的数量字段。如果数量为2K,那么我必须创建成千上万个“票证”。当我减少数量时,我必须删除一些票证。这对我来说听起来完全不轻松。在基本用例中有很多头疼的问题。也许我错过了什么,但为什么不恢复以前删除的事务行为,只需使用类似于_bulk_docs?reject_on_conflict=true的可选项即可。在单主配置中非常有用。 - Sam
4
请阅读:http://guide.couchdb.org/draft/recipes.html,答案归结于couchdb的内部数据结构,“您从不更改数据,只需添加新数据”。在您的情况下,这意味着创建一个(原子性)交易,将账户转入中转账户以进行借方处理,然后再创建第二个(原子性)交易,将资金从中转账户转出。这就是真正的银行操作方式,每个步骤都有记录。 - Fabian Zeindl
显示剩余7条评论

27

对MrKurt答案的进一步拓展。在许多情况下,您不需要按顺序兑换库存票。而是可以从剩余的票中随机选择,而不是选择第一个票。如果有大量票和大量并发请求,则与每个人都尝试获取第一个票相比,这些票的争用将大大减少。


23

一种针对RESTful事务的设计模式是在系统中创建"紧张"。对于一个常见的银行账户交易用例,您必须确保更新涉及到的两个账户的总额:

  • 创建一个交易文档"从帐户11223转账10美元到帐户88733"。这样就在系统中创建了紧张。
  • 为了解决任何紧张,扫描所有事务文档:
    • 如果源账户还没有被更新,则更新源账户(-10美元)
    • 如果源账户已经更新,但交易文档没有显示出来,则更新交易文档(例如,在文档中设置标志"sourcedone")
    • 如果目标账户还没有被更新,则更新目标账户(+10美元)
    • 如果目标账户已经更新,但交易文档没有显示出来,则更新交易文档
    • 如果两个账户都已经更新,则可以删除交易文档或保留其进行审计。

扫描紧张状态应该在后端进程中完成,适用于所有"紧张文件",以保持系统中的紧张时间短暂。在上面的示例中,当第一个帐户已更新但第二个帐户尚未更新时,预计会存在短时间的不一致。如果您的CouchDB是分布式的,就必须考虑这一点,以同样的方式处理最终一致性。

另一种可能的实现方法完全避免了需要使用事务:只需存储紧张文档,并通过评估每个涉及到的紧张文档来评估系统的状态。在上面的例子中,这意味着账户的总额仅由涉及到该账户的交易文档中的值之和确定。在Couchdb中,您可以将其很好地建模为一个映射/减少视图。


6
账户被扣款但张力文档未更改的情况怎么办呢?如果这两个环节之间不是原子性的话,任何故障场景都会导致永久的不一致对吧?因此,在流程中必须有原子性,这就是事务的目的。 - Ian Varley
4
想象一下每个张力文件都有一个时间戳,而账户文件有一个“上次使用张力”的字段 - 或应用张力的列表。当您从源账户借记时,还要更新“上次使用张力”字段。这两个操作是原子操作,因为它们在同一文档上进行。目标账户也有类似的字段。这样系统就可以始终确定哪些张力文件已经应用于哪些账户。 - Jesse Hallett
1
如何检测源/目标文档是否已更新?如果在第一步失败,然后重新执行并再次失败,以此类推,您将继续扣除源帐户? - wump
1
@wump:您需要记录张力文档已应用于账户。例如,通过将张力文档ID附加到任一账户的列表属性上。当所有受张力文档影响的账户都已更新后,将张力文档标记为“完成”或删除它。之后,可以从所有账户的列表中删除文档ID。 - ordnungswidrig
谢谢!这让我明白了。只需在一个单独的字段中暂时保留应用张力文档的列表,该列表可以原子地更新。'last-tension-applied'也可以工作,但更难,因为它假定单调递增排序,在分布式设置中需要特别注意。 - wump
显示剩余4条评论

7
不,CouchDB通常不适用于事务性应用,因为它在集群/复制环境中不支持原子操作。CouchDB在可扩展性方面牺牲了事务能力。为了进行原子操作,您需要一个中央协调系统,这会限制您的可扩展性。如果您可以保证只有一个CouchDB实例或者每个修改特定文档的人连接到同一个CouchDB实例,那么您可以使用冲突检测系统来创建一种类似原子性的方法,如上所述。但是,如果您稍后扩展到集群或使用像Cloudant这样的托管服务,它将会崩溃,您将不得不重新做系统的那部分。因此,我的建议是,对于您的账户余额,使用其他东西而不是CouchDB,这样会更容易些。

6
作为对OP问题的回应,Couch可能不是最佳选择。使用视图是跟踪库存的好方法,但将其限制为0几乎是不可能的。问题在于当您读取视图结果时,决定使用“hammer-1”项目,然后编写一个文档来使用它时会出现竞争条件。问题在于,如果视图的结果是有> 0个锤子1,则没有原子方式仅将文档写入以使用锤子。如果100个用户同时查询视图并看到1个锤子1,则他们都可以编写一个文档以使用锤子1,导致-99个锤子1。实际上,竞争条件将非常小-如果您的DB正在本地主机上运行,则非常小。但是一旦您扩展并拥有离线DB服务器或集群,问题将变得更加明显。无论如何,在关键的与金钱相关的系统中具有这种竞争条件是不可接受的。
对MrKurt的回应进行更新(可能只是过时,或者他可能不知道某些CouchDB功能)
视图是在CouchDB中处理余额/库存等事物的好方法。
你不需要在视图中发出docid和rev。当您检索视图结果时,您会免费获得这两个内容。发出它们-特别是在像字典这样冗长的格式中-只会使您的视图变得不必要的庞大。
用于跟踪库存余额的简单视图应该看起来更像这样(也是我脑海中的想法)。
function( doc )
{
    if( doc.InventoryChange != undefined ) {
        for( product_key in doc.InventoryChange ) {
            emit( product_key, 1 );
        }
    }
}

而且reduce函数甚至更加简单

_sum

这里使用了一个内置的reduce函数(built in reduce function),它只对所有具有匹配键的行的值求和。
在此视图中,任何文档都可以有一个成员“InventoryChange”,它将product_key映射到其总库存的变化。例如:
{
    "_id": "abc123",
    "InventoryChange": {
         "hammer_1234": 10,
         "saw_4321": 25
     }
}

将添加10个hammer_1234和25个saw_4321。

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

将从库存中烧掉5个锤子。

使用这种模型,您永远不会更新任何数据,只会进行追加。这意味着没有更新冲突的机会。所有更新数据的事务问题都消失了 :)

这种模型的另一个好处是,数据库中的任何文档都可以向库存添加和减少物品。这些文档可以包含各种其他数据。您可能有一个“装运”文档,其中包含有关收到日期和时间、仓库、接收员工等的大量数据,并且只要该文档定义了InventoryChange,它就会更新库存。销售文件、损坏商品文件等也可以。查看每个文档时,它们都读得非常清楚。视图处理所有繁重的工作。


有趣的策略。作为一个CouchDB新手,似乎为了计算当前的锤子数量,你需要在公司对于锤子库存的整个历史记录上执行一次map/reduce操作。这可能涉及多年的更改。CouchDB中是否有一些内置功能可以提高性能? - chadrik
是的,CouchDB中的视图就像是连续不断的、持久的映射/归约。您说得对,从头开始处理大型数据集会花费很长时间,但是当添加新文档时,它们仅会更新现有视图,而无需重新计算整个视图。请注意,视图需要空间和CPU资源。此外,至少在我专业使用CouchDB时(已经过去几年了),只使用内置的reduce函数(例如_sum)非常重要。自定义JavaScript reduce函数非常慢。 - wallacer

2
实际上,你可以通过一定的方式实现。请查看HTTP文档API并向下滚动到“使用单个请求修改多个文档”标题。

基本上,您可以在单个post请求中创建/更新/删除一堆文档,URI为 / {dbname} / _bulk_docs ,它们将全部成功或全部失败。但是,该文档警告说这种行为可能会在未来发生改变。

编辑:如预测的那样,在0.9版本中,批量文档不再以这种方式工作。


这并不能真正帮助到正在讨论的情况,即多个用户对单个文档的争用。 - Kerr
3
从CouchDB 0.9开始,批量更新的语义已经发生了变化。 - Barry Wark

0

只需使用轻量级的SQlite解决方案来处理事务,当事务成功完成后进行复制,并在SQLite中标记为已复制

SQLite表

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

您还可以删除已成功复制的交易。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接