Javascript 当多个服务器访问数据库时,如何使用mongodb只允许一个条目?

Javascript 当多个服务器访问数据库时,如何使用mongodb只允许一个条目?,javascript,mongodb,mongoose,Javascript,Mongodb,Mongoose,我有多个“工作”服务器处理作业并访问同一个MongoDB数据库,但我只希望创建一条消息,决不允许运行同一作业的两个服务器创建同一条消息 发送消息时,其状态字段设置为“已发送”,或者如果已禁用,则设置为“已禁用”。因此,它首先检查是否有任何已发送或已禁用的消息。然后,它创建一个文档,将lockedAt字段设置为当前时间,并检查同一消息是否已被锁定。我使用lockedAt字段的原因是,如果作业因某种原因失败,它将允许锁过期并再次运行 这似乎在大多数情况下都有效,但如果两个“工作者”在几毫秒内运行同一

我有多个“工作”服务器处理作业并访问同一个MongoDB数据库,但我只希望创建一条消息,决不允许运行同一作业的两个服务器创建同一条消息

发送消息时,其
状态
字段设置为“已发送”,或者如果已禁用,则设置为“已禁用”。因此,它首先检查是否有任何已发送或已禁用的消息。然后,它创建一个文档,将
lockedAt
字段设置为当前时间,并检查同一消息是否已被锁定。我使用
lockedAt
字段的原因是,如果作业因某种原因失败,它将允许锁过期并再次运行

这似乎在大多数情况下都有效,但如果两个“工作者”在几毫秒内运行同一个作业,则会有一些消息通过,因此我的逻辑并不完美,但我无法确定重复消息是如何创建的

如何使用MongoDB防止同一作业在同一时间运行并两次创建同一消息

// Check if there is a locked message.
// insert a new message or update if one is found but return old message (or nothing if one didn't' exist)
const messageQuery = {
    listingID: listing._id,
    messageRuleID: messageRule._id,
    reservationID: reservation._id
};

let message = await Message.findOne({
    ...messageQuery,
    ...{status: {$in: ["disabled", "sent"]}}
});
// If message has been sent or is disabled don't continue
if (message) {
    return;
}

message = await Message.findOneAndUpdate(
    messageQuery,
    {
        listingID: listing._id,
        messageRuleID: messageRule._id,
        reservationID: reservation._id,
        lockedAt: moment().toDate() // Check out the message with the current date
    },
    {upsert: true}
);
// If no message is defined, then it's new and not locked, move on.
if (message) {
    // If message has been locked for less than X minutes don't continue
    const cutoff = moment().subtract(
        Config.messageSendLock.amount,
        Config.messageSendLock.unit
    );
    if (message.lockedAt && moment(message.lockedAt).isAfter(cutoff)) {
        // Put the last lock time back
        await Message.findOneAndUpdate(messageQuery, {lockedAt: message.lockedAt});
        return;
    }
}

创建一个单独的集合,例如
jobs

无论何时开始处理文档,都可以将_id等于文档id的内容插入到
作业

处理完成后,删除作业


在作业中设置相应的
lockedAt

比这简单得多。在蒙哥

您所需要做的就是为消息定义一个唯一的键

假设关键是:

const messageId = {
    listingID: listing._id,
    messageRuleID: messageRule._id,
    reservationID: reservation._id
};
然后您只需要2个写查询。第一个是“插入”上插入的一半:

try {
    await Message.create(messageId);
} catch (err) {
    if (err.code !== '11000') {  // ignore duplicate key error
        throw err
    }
}
const cutoff = moment().subtract(
    Config.messageSendLock.amount,
    Config.messageSendLock.unit
);
    

message = await Message.findOneAndUpdate(
    {
        ...messageId,
        status: {$nin: ["disabled", "sent"]},
        $or: [
            { lockedAt: {$exists: false} }, 
            { lockedAt: {$lte: cutoff}}
        ]
    },
    { lockedAt: moment().toDate() }
);

// If no message is defined, then it's either sent, disabled, or is locked
if (!message) {
    return
}
这是幂等的,可以由多个工作进程同时执行。结果总是相同的-集合中正好有一个文档

该索引可定义如下:

Message.index({
        listingID: 1,
        messageRuleID: 1,
        reservationID: 1
}, { unique: true })
重要信息:确保不仅在模型上定义了唯一索引,而且索引已应用于数据库端的集合。Mongoose有一些优化,并不总是应用索引。否则,上面的代码将创建重复项。以下文件中的相关部分:

当应用程序启动时,Mongoose会自动为模式中定义的每个索引调用createIndex。Mongoose将按顺序为每个索引调用createIndex,并在所有createIndex调用成功或出现错误时在模型上发出“index”事件。虽然对于开发来说很不错,但建议在生产中禁用此行为,因为索引创建可能会对性能产生重大影响。通过将架构的“自动索引”选项设置为false来禁用该行为,或者通过将“自动索引”选项设置为false来全局禁用连接

第二个查询是upsert的“更新”部分:

try {
    await Message.create(messageId);
} catch (err) {
    if (err.code !== '11000') {  // ignore duplicate key error
        throw err
    }
}
const cutoff = moment().subtract(
    Config.messageSendLock.amount,
    Config.messageSendLock.unit
);
    

message = await Message.findOneAndUpdate(
    {
        ...messageId,
        status: {$nin: ["disabled", "sent"]},
        $or: [
            { lockedAt: {$exists: false} }, 
            { lockedAt: {$lte: cutoff}}
        ]
    },
    { lockedAt: moment().toDate() }
);

// If no message is defined, then it's either sent, disabled, or is locked
if (!message) {
    return
}

同时从多个工作者运行此查询将在数据库端的匹配文档上产生一个。第一个将添加/更新
lockedAt
字段,并将文档返回给工作人员。所有连续的查询都不会匹配筛选器,并且不会返回任何内容。

我遇到了相同的问题,并使用了类似的解决方案。一台服务器上的作业被移动了1-2分钟,这样我可以防止这种冲突。您将
\u id
设置为什么?@WernfriedDomscheit我有数千个作业,虽然我不希望同一个作业同时运行,但我无法控制它。@Yahya,
\u id
是由MongoDB设置的。它类似于此
5fe3543b4477fe13efa83286
。为什么要问?
\u id
是由驱动程序设置的,而不是MongoDB。此外,您需要在一个调用中执行所有操作。如果您使用的是4.2+版本,请考虑在您的第一个请求中添加更多复杂性,或者进行聚合更新。谢谢您的帮助。我不确定这怎么会比我现在用的更好。我需要一些方法来保证如果两个作业同时运行,它们不会创建重复的
消息
文档谢谢!如何定义唯一键?尽管集合中已经存在数千个条目,但是否可以定义唯一的密钥事件?当然可以。可以从现有集合中添加和删除索引。请参阅我关于如何在Mongoose模式中定义索引的更新。还要确保您阅读了文档中引用的摘录。考虑到Stackoverflow上关于mongoose索引的许多问题,其行为并不完全直观。感谢您解释关于唯一索引的问题,我在另一个项目中遇到了这个问题,我将它添加到代码中,但它似乎没有生效。我想我从未验证过它是否保存到数据库中。最后一个问题。某些消息文档可能具有相同的
{listingID,messageRuleID,reservationID}
,但不同的
状态
字段。有没有办法考虑到这一点?是的,这是技术中最关键的部分。这就是为什么我开始回答“你所需要做的就是为你的消息定义一个唯一的键”。它是您的应用程序所独有的,并且与业务逻辑紧密耦合,从问题来看,业务逻辑显然不是那么明显。我假设标准FSM的状态代表当前状态。如果你需要帮助定义索引,恐怕你需要详细说明它是如何工作的。亚历克斯,你有时间看看下面链接的问题吗?这与这个问题有关,但我似乎找到了一种方法,可以使用多个服务器同时连接到同一个数据库来绕过锁代码。