如何确保两个用户可以原子地确认交易已经发生在 mongodb

How to ensure two users can atomically confirm transaction has taken place in mongodb

我有一个名为事务的模型,它具有以下架构

var transactionSchema = new mongoose.Schema({
    amount: Number,
    status: String,
    _recipient: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
    _sender: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
});

我希望此交易的发件人和收件人都能够'confirm'交易发生。 status 开始时是 "initial"。因此,当只有发件人确认交易(但收件人尚未确认)时,我想将 status 更新为 "senderConfirmed" 或其他内容,并且当收件人已确认(但发件人尚未确认)时,我想将状态更新为 "recipientConfirmed"。当他们 both 确认后,我想将状态更新为 "complete".

问题是,我如何知道何时以避免竞争条件的方式将其更新为 "complete"?如果发送方和接收方同时去确认交易,那么两个线程都会认为状态是 "initial" 并更新为 "senderConfirmed" 或 "recipientConfirmed",而实际上它应该去 "complete".

我了解了 MongoDB 的两阶段提交方法 here 但这不太符合我的需要,因为我不想(在另一个线程当前正在修改事务的情况下)阻止第二个线程不进行更新 - 我只希望它 等待 直到第一个线程在进行更新之前完成,然后使其更新的内容取决于事务的最新状态.

更新:我正在根据评论澄清我的答案,我的原始回复没有提供答案。

另一种方法是将状态作为两个单独的属性进行跟踪:

senderConfirmed: true/false,
recipientConfirmed: true/false,

当发件人确认您只需更新 senderConfirmed 字段。当收件人确认您更新 recipientConfirmed 字段时。他们不可能互相覆盖。

要确定交易是否完成,您只需查询 {senderConfirmed:true,recipientConfirmed:true}.

显然这是对文档架构的更改,因此可能并不理想。

原答案:

是否可以更改您的架构?如果您有两个属性 - senderStatusrecipientStatus 怎么办?发件人只会更新 senderStatus,收件人只会更新 recipientStatus。然后他们就无法覆盖彼此的更改。

我想您仍然需要一些其他方法来将其标记为完成。你可以给我们做一个 cron 作业什么的...

底线是您需要 "two" 更新语句来分别为每个发件人和收件人执行此操作。所以基本上一个人会尝试将 "partial" 状态设置为完成,而另一个只会将 "initial" 状态匹配设置为 "partial" 状态。

批量操作是实现多条语句的最佳方式,因此您应该通过访问底层驱动程序方法来使用它们。现代 API 版本具有 .bulkWrite() 方法,如果服务器版本不支持 "bulk" 协议,该方法会很好地降级,然后退回到发布单独的更新。

// sender confirmation
Transaction.collection.bulkWrite(
    [
       { "updateOne": {
           "filter": {
               "_id": docId,
               "_sender": senderId,
               "status": "recipientConfirmed"
           },
           "update": {
               "$set": { "status": "complete" }
           }
       }},
       { "updateOne": {
           "filter": {
               "_id": docId,
               "_sender": senderId,
               "status": "initial"
           },
           "update": {
               "$set": { "status": "senderConfirmed" }
           }
       }}
    ],
    { "ordered": false },
    function(err,result) {
       // result will confirm only 1 update at most succeeded
    }
);

当然同样适用于 _recipient 除了不同的状态检查或更改。您可以交替地在 _sender_recipient 上发出 $or 条件并具有通用 "partial" 状态而不是编码不同的更新条件,但相同的基本 "two update"流程适用。

当然,您再次 "could" 只是使用常规方法并以另一种方式向服务器发出两个更新,甚至可能是并行的,因为条件仍然存在 "atomic",但这也是原因对于 { "ordered": false } 选项,因为它们没有确定的顺序,需要在此处遵守。

虽然批量操作比单独调用要好,因为发送和 return 只是 一个 请求和响应,而不是每个 "two" ,因此使用批量操作的开销要少得多。

但这是一般方法。在另一方也发出确认之前,任何一个声明都不可能在 "deadlock" 中留下 "status" 或标记为 "complete"。

有一个 "possibility" 和一个非常苗条的状态,在第一次尝试更新和第二次尝试更新之间从 "initial" 更改了状态,这将导致没有任何更新。在这种情况下,您可以 "retry" 其 "should" 在后续尝试中更新的操作。

不过,这最多只需要 "one" 重试。而且非常非常少。


注意:在 Mongoose 模型上使用 .collection 访问器时应小心。所有常规模型方法都内置逻辑 "ensure" 在它们执行任何操作之前实际上存在与数据库的连接,并且实际上 "queue" 操作直到存在连接。

将应用程序启动包装在事件处理程序中以确保数据库连接通常是一个好习惯:

mongoose.on("open",function() {
    // App startup and init here
})

所以在这种情况下使用 "on""once" 事件。

通常,在触发此事件后或在应用程序中已调用任何 "regular" 模型方法后,连接始终存在。

可能 mongoose 会在未来的版本中直接在模型方法中包含 .bulkWrite() 等方法。但目前还没有,所以 .collection 访问器是从核心驱动程序中获取底层 Collection 对象所必需的。