Javascript 当多个服务器访问数据库时,如何使用mongodb只允许一个条目?
我有多个“工作”服务器处理作业并访问同一个MongoDB数据库,但我只希望创建一条消息,决不允许运行同一作业的两个服务器创建同一条消息 发送消息时,其Javascript 当多个服务器访问数据库时,如何使用mongodb只允许一个条目?,javascript,mongodb,mongoose,Javascript,Mongodb,Mongoose,我有多个“工作”服务器处理作业并访问同一个MongoDB数据库,但我只希望创建一条消息,决不允许运行同一作业的两个服务器创建同一条消息 发送消息时,其状态字段设置为“已发送”,或者如果已禁用,则设置为“已禁用”。因此,它首先检查是否有任何已发送或已禁用的消息。然后,它创建一个文档,将lockedAt字段设置为当前时间,并检查同一消息是否已被锁定。我使用lockedAt字段的原因是,如果作业因某种原因失败,它将允许锁过期并再次运行 这似乎在大多数情况下都有效,但如果两个“工作者”在几毫秒内运行同一
状态
字段设置为“已发送”,或者如果已禁用,则设置为“已禁用”。因此,它首先检查是否有任何已发送或已禁用的消息。然后,它创建一个文档,将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的状态代表当前状态。如果你需要帮助定义索引,恐怕你需要详细说明它是如何工作的。亚历克斯,你有时间看看下面链接的问题吗?这与这个问题有关,但我似乎找到了一种方法,可以使用多个服务器同时连接到同一个数据库来绕过锁代码。