You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

MongoDB并发写入时UpsertId后无法用query.Count()验证的重复报价问题

这个问题我太熟悉了——本质上是竞态条件在搞鬼!当用户快速双击发送多个请求时,你的应用层「检查是否存在相同assetId的活跃报价」和「插入新报价」这两步不是原子操作,就会出现以下连锁反应:

  1. 请求A先查数据库,发现没有assetId=X的活跃报价
  2. 几乎同时,请求B也查数据库,这时候请求A还没完成插入,所以也返回“不存在”
  3. 最后两个请求都执行了插入,就出现了重复的活跃报价

最靠谱的解决方案:数据库层面加唯一索引

应用层的验证永远赶不上并发的速度,只有数据库层面的约束才是最可靠的。结合你的需求(仅活跃报价需要唯一),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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 04:20:47