
本文详解如何通过唯一复合索引配合错误捕获机制,在高并发 goroutine 场景下可靠防止重复插入同名同姓的 person 文档,彻底规避“检查-插入”竞态缺陷。
本文详解如何通过唯一复合索引配合错误捕获机制,在高并发 goroutine 场景下可靠防止重复插入同名同姓的 person 文档,彻底规避“检查-插入”竞态缺陷。
在分布式或高并发 Go 应用中,多个 Goroutine 同时尝试向 MongoDB 的 persons 集合插入相同 name 和 lastName 的人员记录,极易引发数据重复问题。常见误区是采用「先查后插」逻辑(即 Find() 判断是否存在,再 Insert()),但由于 MongoDB 缺乏跨操作事务支持,两次操作之间存在时间窗口——另一 Goroutine 可能恰好在此间隙完成插入,导致最终写入多条重复文档。
根本解法:唯一索引 + 写时容错处理
MongoDB 唯一索引(Unique Index)是原子级的底层约束,能在插入瞬间由存储引擎直接校验并拒绝冲突写入。这是唯一能真正覆盖多进程、多服务、多节点等全场景并发竞争的可靠方案。
✅ 正确实施步骤
-
创建唯一复合索引
在集合初始化阶段(如应用启动时),确保为 name 与 lastName 字段建立升序唯一索引:
index := mgo.Index{
Key: []string{"name", "lastName"},
Unique: true,
// 可选:启用后台构建,避免阻塞主业务
Background: true,
}
err := collection.EnsureIndex(index)
if err != nil {
log.Fatal("Failed to create unique index:", err)
}⚠️ 注意:EnsureIndex 是幂等操作,多次调用仅生效一次;建议在服务启动时执行,而非每次插入前调用。
-
插入时主动捕获重复错误
使用 Insert() 执行写入,并通过 mgo.IsDup() 准确识别唯一键冲突(而非泛化 err != nil 判断):
person := Person{
Id: bson.NewObjectId(),
Name: "Alice",
LastName: "Smith",
}
err := collection.Insert(&person)
if err != nil {
if mgo.IsDup(err) {
// ✅ 安全捕获:已存在同名同姓记录
log.Printf("Duplicate person detected: %s %s", person.Name, person.LastName)
// 可返回已有文档ID、重定向至详情页,或静默忽略
return nil // 或返回特定错误码
}
// ❌ 其他错误(如网络中断、权限不足等),需按业务逻辑处理
return fmt.Errorf("insert failed: %w", err)
}
// 插入成功
log.Printf("New person created: %v", person.Id)? 关键注意事项
- 绝不依赖“查后再插”:Find().Count() → Insert() 的模式在并发下必然失效,属于反模式;
- 索引字段顺序无关语义,但影响查询效率:若常按 lastName 查询,可将 "lastName", "name" 设为索引键序以支持高效范围扫描;
- 区分错误类型至关重要:mgo.IsDup() 仅识别 E11000 duplicate key 类错误,其他错误(如连接超时)必须单独处理,不可一概而论;
- 考虑业务语义完整性:唯一索引仅保证字段组合不重复,若需同时校验邮箱、身份证号等多维度唯一性,应建立对应索引并统一捕获逻辑;
- 升级提醒:mgo 已归档,生产环境建议迁移到官方驱动 mongo-go-driver,其错误分类更精细(如 mongo.IsDuplicateKeyError(err)),且支持会话与事务增强。
✅ 总结
解决并发重复插入的核心在于将一致性保障下沉至数据库层:通过唯一索引强制约束 + 应用层优雅降级,而非在应用逻辑中模拟事务。该方案简洁、高效、可扩展,是 MongoDB 并发写入场景下的标准实践。










