分片键不包含\_id时duplicate key错误源于路由失效:\_id未被分片键约束,导致相同\_id文档被写入不同分片;若\_id为分片键却仍报错,则多因客户端直连分片绕过mongos路由。

分片键不包含 _id 时 duplicate key 错误的根源
在分片集群中看到 E11000 duplicate key error collection: db.coll index: _id_ dup key: { _id: ObjectId("...") },第一反应不是数据重复,而是分片键设计缺陷。MongoDB 要求所有写入操作(包括插入)必须能路由到唯一目标分片,而这个路由依赖分片键。如果 _id 没被选为分片键,且应用层又手动指定 _id 值(比如用业务 ID 或 UUID 字符串),就可能出现两个不同分片各自接受了一个相同 _id 的文档——它们在各自分片内不冲突,但全局看就是重复主键。
- 检查分片键:运行
sh.status(),确认集合的分片键是否包含_id;若为{ "user_id": 1 }这类业务字段,则_id完全不受分片逻辑约束 - 禁止手动设
_id:除非你明确用ObjectId或UUID且确保生成逻辑全局唯一,否则让 MongoDB 自动创建_id - 验证写入路径:用
mongos的日志开启verbosity: 2,过滤write相关条目,看同个_id是否被发往了多个分片
分片键是 _id 但仍有冲突的典型场景
即使把 _id 设为分片键(如 sh.shardCollection("db.coll", { _id: "hashed" })),仍可能报重复错误——这通常是因为客户端绕过了 mongos,直连某个分片节点执行插入。此时写入不经过路由判断,直接落盘,mongos 无法保证全局唯一性。
- 抓包或查连接日志:确认应用连接的是
mongos地址(如mongodb://mongos1:27017,mongos2:27017/...),而非单个shard的地址(如mongodb://shard1a:27018/...) - 检查驱动配置:Spring Boot 中若误配了
spring.data.mongodb.host指向 shard 节点,就会跳过路由层 - 测试验证:临时停掉一个分片,再执行插入,如果操作仍成功,说明流量没走
mongos
如何快速定位冲突文档落在哪个分片
报错信息里只给了 _id 值,但不知道它实际存在哪几个分片上。别急着删数据,先用 sh.findChunkFor() 和 db.runCommand({ getShardVersion: "db.coll" }) 定位分片归属,再登录对应分片查真实文档。
- 查路由归属:在
mongos上执行sh.findChunkFor("db.coll", { _id: ObjectId("60a1b2c3d4e5f67890123456") }),返回的shard字段即目标分片名 - 查实际存在情况:用
mongo --host shard1a:27018直连该分片,执行db.coll.findOne({ _id: ObjectId("...") });重复查其他分片确认是否多处存在 - 注意权限:直连分片需有
clusterAdmin或root角色,普通应用账号无权访问
避免后续再出现的关键配置与习惯
这类问题一旦发生,修复成本高、影响面广。真正有效的预防不在事后排查,而在部署初期就堵死路径。
- 强制使用 hashed _id 分片键:对新集合,优先用
{ _id: "hashed" },它天然支持全局唯一路由,且无需业务改造 - 禁用直连分片:在分片节点的
mongod.conf中设置setParameter: { enableLocalhostAuthBypass: false },并移除所有指向 shard 的 DNS 别名或连接字符串 - 监控写入分布:用
sh.status()中的 chunk 分布 +db.printShardingStatus()配合 Prometheus 抓取shardInserts指标,发现某分片写入量长期为 0 就说明路由异常











