upstart导致E11000重复密钥错误



下面的upstart语句不起作用,得到:

MongoServerError: E11000 duplicate key error collection: db.emails index: _id_ dup key: { _id: "8hh58975fw" }

我的目标是用相同的_id编辑现有文档(或创建(如果存在))api中的代码是:

await db.collection('emails')
.updateOne(
{
_id: userId,
type: "profileCompletion",
time: new Date(),
},
{
$inc: { "count": 1},
},
{upsert: true},
)

但我从这个工作中应对了它:

await db.collection('analytics')
.updateOne(
{
_id: getDateMonYear(new Date(), "mmddyyyy"),
type: "Geo",
},
{
$inc: { "count": 1},
},
{upsert: true},
)

正如@Kal在回答中分享的那样,这个特定问题中的主要问题与time: new Date()的使用有关。这个答案旨在帮助探索为什么会出现这种情况,帮助澄清在该答案中调整后的update中并发如何仍然是一个问题,以及更安全的update操作可能是什么样子。

Upsert修改

当作为upsert的结果插入新文档时,有几种方法可以构建新文档。这一点目前记录在这里。与这种情况相关的是">设置";使用更新运算符的行为(由于更改被定义为{ $inc: { "count": 1}, })。相关段落:

如果没有文档与查询条件匹配,并且<update>参数是具有更新运算符表达式的文档,则该操作将根据<query>参数中的相等子句创建一个基础文档,并应用<update>参数中的表达式。

因此,给定123的(新)userId,1月1日执行的upsert将查询集合,查找与以下内容匹配的文档:

{
_id: 123,
time: ISODate("2022-01-01..."),
type: 'profileCompletion'
}

如果没有,upsert行为将触发,操作将向集合中插入类似于以下内容的文档:

{
_id: 123,
time: ISODate("2022-01-01..."),
type: 'profileCompletion',
count: 1
}

现在,我们继续讨论在操作的后续执行中会发生什么。

Upsert匹配

假设一个月后的2月初执行相同的操作(123userId)

{
_id: 123,
time: ISODate("2022-02-01..."),
type: 'profileCompletion'
}

虽然具有给定_id(和type)的文档确实存在,但由于time的差异,整个文档不匹配。与之前类似,这会触发upsert行为,操作会尝试插入以下文档:

{
_id: 123,
time: ISODate("2022-02-01"),
type: 'profileCompletion',
count: 1
}

当然,由于包含123_id值的现有文档而导致上述重复密钥错误,这会失败。换句话说,问题之一是每次执行此操作时时间戳都不同。

因此,从操作的<query>部分删除time: new Date()的更改同时做两件事:

  1. 它消除了最初插入的文档中的time字段
  2. 当匹配时,它随后删除<query>的该组件,从而导致现有文档按预期更新

并发性

当我在评论中声称您遇到的问题是并发的结果时,我(显然)是不正确的。更正确地说,我应该指出它可能是并发的结果。在更改为从操作中删除time之后,这一情况仍然存在。

关于追加销售的文档包括以下注释:

如果大致同时发布多个相同的追加订单,则update()upsert: true一起使用可能会创建重复的文档。

然后链接到另一个以开头的部分

使用update()方法的upsert: true选项,并且不在查询字段上使用唯一索引,在某些情况下,具有相似查询字段的update()操作的多个实例可能会导致插入重复文档。

我最初指出该文档是";证明;并发性可能是一个问题。我相信@Kal最初正确地指出,本节末尾的以下文本暗示第二次操作应如预期的那样update(增加了强调):

有了这个唯一的索引update()操作现在表现出以下行为:

  • 只需执行一次update()操作即可成功插入新文档
  • 所有其他update()操作都将更新新插入的文档,增加分值

这对我来说是个新闻!我回到过去,看了看4.0版本的该部分的措辞(再次强调):

剩下的操作将是:

  • 更新新插入的文档,或者
  • 尝试插入重复项时失败。如果操作因重复索引键错误而失败,应用程序可以重试该操作,该操作将作为更新操作成功

对于版本4.2,该文本已更改。经过一番挖掘,我发现了这个名为">重试update+upstart的谓词唯一索引冲突->尽可能插入";。问题描述表明,在4.2的某些情况下,对这种行为进行了一些改进。

替代(更安全)重写

上述改进表明,服务器现在可以透明地重试在某些条件下遇到重复密钥异常的upsert。该票证中有一个表列出了这些条件,但在我看来,它们就像是写入之间并发的结果,而不是错误的其他原因,包括这个特定问题中的time: new Date()问题。

事实上,我们可以看到,即使是另一个答案中的修改操作,如果遇到重复密钥异常,也不会透明地重试。如果我理解正确的话,修改后的操作现在将具有<query>CCD_ 41。这属于该表中描述的第6行,因为type不是唯一索引定义的一部分。

在任何情况下,虽然修改后的操作不再在没有并发的情况下生成重复密钥异常,但您可能仍然希望重写该操作。插入文档时,您仍然可以向文档添加时间戳,但不应在操作的<query>部分中指定时间戳。相反,它应该通过$set$setOnInsert表示,作为所描述的更改的一部分,具体取决于操作执行update时您想要的行为

大致如下:

await db.collection('emails')
.updateOne(
{
_id: userId
},
{
$setOnInsert: 
{
type: "profileCompletion",
time: new Date()
},
$inc: { "count": 1},
},
{upsert: true},
)

感觉文档有点误导,所以我会在网站上发送一些反馈。

解决方案是删除:

time: new Date(),

来自查询:

await db.collection('emails')
.updateOne(
{
_id: userId,
type: "profileCompletion",
time: new Date(),
},
{
$inc: { "count": 1},
},
{upsert: true},
)

这确实是我的问题中工作问题和非工作问题之间的区别。

摘要:只要有唯一的索引,无论id是什么,都可以使用相同的_id进行Upsert。在我的案例中,_id是派生的,并且是相同的,加上它是唯一的索引。

上面评论的用户发布的链接是有效的,但关于多线程等的评论与此问题无关

最新更新