唯一索引校验在高并发下引发写冲突和锁排队,因wiredtiger需加读锁检查值存在性,多事务同时插入相同唯一值会导致wt_rollback;_id索引有fast-path优化,而自定义唯一索引走通用校验流程并争抢索引页latch,复合索引若前缀集中更易形成热点。

不是事务本身慢,是唯一索引校验在高并发下触发写冲突和锁排队。
唯一索引插入时为何引发 WT_ROLLBACK 和重试
在 MongoDB 5.0+(WiredTiger 引擎)中,插入文档时若字段上有 unique: true 索引,引擎必须先检查该值是否已存在——这个检查操作会加读锁;而如果多个事务几乎同时插入相同唯一值(比如邮箱、手机号),就会出现「先查到不存在 → 都准备写入 → 第二个被拒绝并回滚」的情况。日志里看到 WT_ROLLBACK 或驱动抛出 DuplicateKeyError 后自动重试,本质是写冲突,不是网络或磁盘慢。
- 冲突不发生在提交阶段,而是在
insertOne()执行瞬间就确定了 -
db.serverStatus().metrics.operation.write.conflicts持续上升是核心指标 - 即使索引命中,只要并发写同一键值范围(如
{email: "a@b.com"}),仍会争抢同一 B-tree 页面的 latch
为什么 _id 索引不会卡,但自定义唯一索引会
_id 是主键索引,写入路径高度优化:WiredTiger 对 _id 的插入做了特殊 fast-path 处理,跳过部分锁校验逻辑;但普通唯一索引(如 createIndex({email: 1}, {unique: true}))走的是通用索引校验流程,且校验过程需持有 index page latch + document lock,容易形成热点。
- 复合唯一索引更危险:
createIndex({tenantId: 1, email: 1}, {unique: true})中,若tenantId值高度集中(如 SaaS 多租户场景下大量用户属于同一租户),所有插入都会挤在同一个索引前缀下 - 索引字段顺序错位:查询用
{email: "x", tenantId: "y"},但索引建的是{tenantId: 1, email: 1}→ 可能触发 COLLSCAN,进一步放大锁持有时间
如何验证和缓解唯一索引导致的事务延迟
先确认是不是它:在事务内执行 db.collection.explain("executionStats").insertOne({...}) 不生效,得改用 find() 模拟校验行为 —— 例如对要插的 email 执行 db.users.find({email: "test@example.com"}).explain("executionStats"),看 executionStats.nReturned 和 totalDocsExamined 是否接近;再查 db.currentOp({ "secs_running": { "$gt": 1 }, "waitingForLock": true }),观察是否有大量事务卡在 insert 操作上。
- 缓解方案优先级:先用
findOneAndUpdate({email: ...}, {...}, {upsert: true, returnDocument: "after"})替代insertOne+ 捕获异常,避免客户端重试逻辑拉长事务生命周期 - 业务层做预检:在事务外用
find().limit(1)快速探查邮箱是否存在,命中则直接走更新分支,不进事务 - 极端高频场景可降级:把唯一性约束从数据库层移到应用层(配合 Redis SETNX 或分布式锁),但需接受小概率双写风险
分片集群下唯一索引的隐藏陷阱
唯一索引在分片集群中默认只保证「单一分片内唯一」,除非你显式声明 sh.shardCollection("db.coll", {email: 1}, {unique: true}) 并确保 email 是分片键——否则跨分片重复插入不会被拦截,而事务又不允许跨分片唯一校验,结果就是数据不一致 + 应用层兜底变复杂。
- 一旦开启分片唯一约束,所有写请求必须带分片键,否则报
IllegalOperation: cannot perform operation with missing shard key - 分片键 + 唯一键组合索引(如
{shardKey: 1, email: 1})能兼顾路由与唯一性,但要注意shardKey基数不能太低,否则还是热点
真正难处理的不是「有没有索引」,而是「唯一性校验在并发写路径上的锁粒度和 latch 争抢位置」——这往往藏在 WiredTiger 的底层页面管理逻辑里,Profiler 和 currentOp 只能告诉你「卡在哪」,没法直接告诉你「怎么改 latch 行为」。所以调优重点永远落在:减少校验次数、缩短校验窗口、避开高竞争键值分布。











