bigywater.tech
MongoDB · 51

并发 upsert 与唯一索引:E11000 从哪来

并发 upsert 会不会重复插入?E11000 什么时候会露给应用?这篇用 16 个线程在同一个条件上并发 upsert,对比有无唯一索引,并给出应用层的处理办法。

632 对 300
没有唯一索引时并发 upsert 后的文档数与应有的文档数
0
有唯一索引时客户端看到的 E11000 次数(服务端自动重试)
1600
16 个线程各 100 次 findOneAndUpdate + $inc 取得的不同编号数

没有唯一索引

实测 · 本机执行输出
  文档总数 632(应为 300 个不同的 k);有重复的 k 有 227 个,最多重复 5 份;错误数 0

upsert 不是原子的“不存在才插入”:并发时多个线程都查不到,各自插入,300 个 k 里 227 个被重复,最多 5 份,而且没有任何错误。

有唯一索引

实测 · 本机执行输出
  文档总数 300(应为 300);客户端收到的错误数 0
  (服务端对“条件里是唯一键的等值”的 upsert 在遇到 E11000 时会自动重试一次,所以一般看不到)

唯一索引挡住重复,文档数正好 300;对条件是唯一键等值的 upsert,服务端遇到 E11000 会自动重试一次,客户端看不到错误。

E11000 从哪来

实测 · 本机执行输出
  3 轮,每轮 16 个线程;文档数 1(唯一的 b="same");错误数 32,例如 ['DuplicateKeyError: Plan executor error during update :: caused by :: E11000 duplicate key', 'DuplicateKeyError: Plan executor error during update :: caused by :: E11000 duplicate key']

条件不包含唯一键(例如多了别的字段,或唯一键值由 $setOnInsert 给出)时,服务端不重试,E11000 会作为 DuplicateKeyError 返回给应用。

应用层重试

实测 · 本机执行输出
  条件 {k, tag: "x"}(多了一个非唯一字段 tag,服务端不再自动重试):捕获到 DuplicateKeyError 22 次,重试更新成功 22 次;最终文档数 60(60 个 k)

捕获 DuplicateKeyError,改成不 upsert 的更新再来一次(此时文档已被别人插入)。

取号器

实测 · 本机执行输出
  16 个线程各取 100 次:共 1600 个编号,不同的 1600 个,最小 1,最大 1600(无重复无缺口)

findOneAndUpdate 加 $inc 是原子的,1600 次得到 1600 个不同的编号。

$setOnInsert

实测 · 本机执行输出
  test> db.s.updateOne({k: 1}, {$set: {v: 1}, $setOnInsert: {created: "first"}}, {upsert: true})
  {
    acknowledged: true,
    insertedId: ObjectId('6acc4b68107af69346916ed2'),
    matchedCount: 0,
    modifiedCount: 0,
    upsertedCount: 1
  }
  test> 第二次 upsert(文档已存在,$setOnInsert 不生效)
  { k: 1, created: 'first', v: 2 }

只在发生插入时设置字段,已存在时保持原值。

分步演示

并发 upsert 与唯一索引
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MongoDB 9.0.2(Homebrew 的 mongod、mongos)/ mongosh 2.12.0 / pymongo 4.18.3;每个实验在临时目录里起全新的 mongod(独立 dbpath,端口 9900 以上,只监听 127.0.0.1,WiredTiger 缓存 0.25 GB),需要副本集或分片集群时在本机起多个进程;命令输出来自真实的 mongosh,批量写入、计时与多线程用 pymongo;计时用 time.perf_counter,数字每次运行会有小幅抖动,ObjectId、时间戳等每次运行都不同。16 个线程用 Barrier 同步出手,重复/错误数每次运行都不同(这次 632、227、22、32)
    • 没有验证:服务端自动重试的精确条件(版本文档里的限制);分片集群与副本集下的并发 upsert;事务里的 upsert 与 WriteConflict(下面事务篇);upsert 配合 _id 的情形;多字段唯一索引上的 upsert 重试
    京ICP备2026067502号-1京公网安备11010502063487号