MongoDB并发写入时UpsertId后无法用query.Count()验证的重复报价问题
这个问题我太熟悉了——本质上是竞态条件在搞鬼!当用户快速双击发送多个请求时,你的应用层「检查是否存在相同assetId的活跃报价」和「插入新报价」这两步不是原子操作,就会出现以下连锁反应:
- 请求A先查数据库,发现没有assetId=X的活跃报价
- 几乎同时,请求B也查数据库,这时候请求A还没完成插入,所以也返回“不存在”
- 最后两个请求都执行了插入,就出现了重复的活跃报价
最靠谱的解决方案:数据库层面加唯一索引
应用层的验证永远赶不上并发的速度,只有数据库层面的约束才是最可靠的。结合你的需求(仅活跃报价需要唯一),MongoDB 3.2+支持的部分唯一索引正好能解决问题——它只会对「isActive为true」的文档强制assetId的唯一性。
用mgo.v2创建部分唯一索引的代码
import ( "fmt" "gopkg.in/mgo.v2" "gopkg.in/mgo.v2/bson" ) // 假设你的报价结构体是这样的 type Quote struct { ID bson.ObjectId `bson:"_id,omitempty"` AssetId string `bson:"assetId"` IsActive bool `bson:"isActive"` // 其他字段... } func initQuoteIndex(session *mgo.Session) error { coll := session.DB("your_db_name").C("quotes") index := mgo.Index{ Key: []string{"assetId"}, Unique: true, PartialFilterExpression: bson.M{"isActive": true}, // 仅对活跃报价生效 Background: true, // 后台创建,不阻塞业务 } return coll.EnsureIndex(index) }
插入时处理重复键错误
当插入重复的活跃报价时,MongoDB会返回E11000重复键错误,你可以在代码中捕获这个错误,给用户友好提示:
func insertQuote(session *mgo.Session, quote *Quote) error { coll := session.DB("your_db_name").C("quotes") err := coll.Insert(quote) if err != nil { // mgo提供了IsDup方法判断是否是重复键错误 if mgo.IsDup(err) { return fmt.Errorf("该资产已有活跃报价,请勿重复提交") } return fmt.Errorf("插入报价失败: %v", err) } return nil }
备选方案:用findAndModify实现原子性检查+插入
如果因为某些限制无法创建索引,你可以用MongoDB的findAndModify操作,把“检查”和“插入”合并成一个原子性操作,避免竞态条件:
func insertQuoteAtomically(session *mgo.Session, assetId string, quote *Quote) error { coll := session.DB("your_db_name").C("quotes") // 定义变更:只有当找不到匹配的活跃报价时,才插入新文档 change := mgo.Change{ Update: bson.M{"$setOnInsert": quote}, Upsert: true, ReturnNew: true, } var result Quote _, err := coll.Find(bson.M{"assetId": assetId, "isActive": true}).Apply(change, &result) if err != nil { return fmt.Errorf("原子插入失败: %v", err) } // 如果返回的文档ID和我们要插入的不一样,说明已经存在活跃报价 if result.ID != quote.ID { return fmt.Errorf("该资产已有活跃报价,请勿重复提交") } return nil }
总结
优先选择部分唯一索引的方案,因为它是数据库层面的约束,代码更简洁,性能也更好,能从根源上避免重复数据。应用层的验证只能作为辅助,不能依赖它来保证并发场景下的唯一性。
内容的提问来源于stack exchange,提问作者Oleksandr Buchek
相关产品推荐
相关产品推荐

