Couchdb 键值存储中的原子事务

Couchdb 键值存储中的原子事务,couchdb,transactions,cassandra,key-value,Couchdb,Transactions,Cassandra,Key Value,请原谅术语上的任何错误。特别是,我使用的是关系数据库术语 有许多持久性键值存储,包括和,以及许多其他项目 反对它们的一个典型论点是,它们通常不允许跨多个行或表进行原子事务。我想知道是否有一个通用的方法可以解决这个问题 以一组银行账户为例。我们如何将资金从一个银行账户转移到另一个银行账户?如果每个银行账户都是一行,我们希望将两行作为同一事务的一部分进行更新,减少其中一行的值,增加另一行的值 一个明显的方法是使用一个单独的表来描述事务。然后,将资金从一个银行帐户转移到另一个银行帐户只需在该表中插入新

请原谅术语上的任何错误。特别是,我使用的是关系数据库术语

有许多持久性键值存储,包括和,以及许多其他项目

反对它们的一个典型论点是,它们通常不允许跨多个行或表进行原子事务。我想知道是否有一个通用的方法可以解决这个问题

以一组银行账户为例。我们如何将资金从一个银行账户转移到另一个银行账户?如果每个银行账户都是一行,我们希望将两行作为同一事务的一部分进行更新,减少其中一行的值,增加另一行的值

一个明显的方法是使用一个单独的表来描述事务。然后,将资金从一个银行帐户转移到另一个银行帐户只需在该表中插入新行即可。我们不存储两个银行账户的当前余额,而是依赖于汇总交易表中所有适当的行。然而,很容易想象这将是太多的工作;一家银行每天可能有数百万笔交易,而一个银行账户可能很快就会有数千笔与之相关的“交易”

如果基础数据自上次抓取以来发生了更改,那么大量(全部?)键值存储将“回滚”操作。这可能用于模拟原子事务,然后,您可以指示某个特定字段被锁定。这种方法存在一些明显的问题


还有其他想法吗?我的方法完全可能是错误的,我还没有用新的思维方式来思考。

以您为例,如果您想在单个文档(关系术语中的行)中自动更新值,您可以在CouchDB中这样做。如果其他争用客户端在您读取同一文档后更新了该文档,则在尝试提交更改时将出现冲突错误。然后必须读取新值,更新并重新尝试提交。您可能需要重复此过程的次数不确定(如果存在大量争用,则可能无限),但如果提交成功,则可以保证数据库中的文档具有原子更新的平衡


如果您需要更新两个余额(即从一个帐户转移到另一个帐户),则需要使用单独的交易文档(实际上是另一个表,其中的行是交易)来存储金额和两个帐户(输入和输出)。顺便说一句,这是一种常见的记账方法。由于CouchDB仅根据需要计算视图,因此从列出该帐户的事务中计算帐户中的当前金额实际上仍然非常有效。在CouchDB中,您将使用一个映射函数,该函数将帐号作为密钥和事务量(传入为正,传出为负)发出。reduce函数只是对每个键的值求和,发出相同的键和总和。然后,您可以使用group=True的视图来获取账户余额,并通过账号进行键入。

CouchDB不适用于事务性系统,因为它不支持锁定和原子操作

为了完成银行转账,您必须做以下几件事:

  • 验证交易,确保源帐户中有足够的资金,两个帐户都已打开、未锁定且信誉良好,等等
  • 减少源帐户的余额
  • 增加目标帐户的余额
  • 如果在这些步骤中的任何一个步骤之间更改了帐户的余额或状态,则交易在提交后可能会变得无效,这在此类系统中是一个大问题

    即使您使用上述方法插入“转账”记录并使用map/REDUCT视图计算最终账户余额,您无法确保不透支源帐户,因为在检查源帐户余额和插入交易(在检查余额后可以同时添加两个交易)之间仍然存在竞争条件

    所以。。。这是做这项工作的错误工具。CouchDB可能擅长很多事情,但这是它真正做不到的


    编辑:可能值得注意的是,现实世界中的实际银行使用最终一致性。如果你透支你的银行账户足够长的时间,你会得到一笔透支费。如果你做得很好,你甚至可以在几乎同一时间从两台不同的自动取款机上取款,并透支你的账户,因为有一个种族条件来检查余额、发款和记录交易。当你将支票存入你的账户时,他们会冲破余额,但实际上会持有这些资金一段时间,以“以防万一”源账户没有足够的资金。

    提供一个具体的例子(因为网上缺少正确的例子):下面是如何在CouchDB中实现一个“”(大部分抄袭自我关于同一主题的博文:)

    首先,简要回顾一下问题:一个允许 在账户之间转账的资金必须设计成不存在种族竞争 可能导致余额无效或无意义的情况

    这个问题有几个部分:

    第一:交易日志。而不是将帐户余额存储在单个帐户中 记录或文档-
    {“账户”:“Dave”,“余额”:100}
    -账户的 余额的计算方法是将该账户的所有贷方和借方相加。 这些贷项和借项存储在一个事务日志中,该日志可能看起来 大概是这样的:

    {"from": "Dave", "to": "Alex", "amount": 50}
    {"from": "Alex", "to": "Jane", "amount": 25}
    
    POST /transactions/balances
    {
        "map": function(txn) {
            emit(txn.from, txn.amount * -1);
            emit(txn.to, txn.amount);
        },
        "reduce": function(keys, values) {
            return sum(values);
        }
    }
    
    def transfer(from_acct, to_acct, amount):
        txn_id = db.post("transactions", {"from": from_acct, "to": to_acct, "amount": amount})
        if db.get("transactions/balances") < 0:
            db.delete("transactions/" + txn_id)
            raise InsufficientFunds()
    
    {"from": "Alex", "to": "Dave", "amount": 100, "timestamp": 50, "status": "successful"}
    {"from": "Dave", "to": "Jane", "amount": 200, "timestamp": 60, "status": "pending"}
    
    而CouchDB map reduce函数可以用来计算平衡 大概是这样的:

    {"from": "Dave", "to": "Alex", "amount": 50}
    {"from": "Alex", "to": "Jane", "amount": 25}
    
    POST /transactions/balances
    {
        "map": function(txn) {
            emit(txn.from, txn.amount * -1);
            emit(txn.to, txn.amount);
        },
        "reduce": function(keys, values) {
            return sum(values);
        }
    }
    
    def transfer(from_acct, to_acct, amount):
        txn_id = db.post("transactions", {"from": from_acct, "to": to_acct, "amount": amount})
        if db.get("transactions/balances") < 0:
            db.delete("transactions/" + txn_id)
            raise InsufficientFunds()
    
    {"from": "Alex", "to": "Dave", "amount": 100, "timestamp": 50, "status": "successful"}
    {"from": "Dave", "to": "Jane", "amount": 200, "timestamp": 60, "status": "pending"}
    
    为了完整起见,这里是
    def transfer(from_acct, to_acct, amount):
        timestamp = time.time()
        txn = db.post("transactions", {
            "from": from_acct,
            "to": to_acct,
            "amount": amount,
            "status": "pending",
            "timestamp": timestamp,
        })
        resolve_transactions(timestamp)
        txn = couch.get("transactions/" + txn._id)
        if txn_status == "rejected":
            raise InsufficientFunds()