应优先选 upsert:抽奖记录属冷数据,需天然防重,upsert 语义更贴合“存在即忽略”;但必须配合唯一索引(如 {uid, activityId, traceId}),且用 mgo.IsDup() 精准识别重复错误。

用 upsert 还是 Insert?别选错场景
抽奖活动记录本质是「写多读少、结构松散、允许延迟」的冷数据,不是用户资料那种强一致性核心数据。所以第一反应不该是“怎么锁住它”,而是“怎么让它天然不重复”。Insert + Find 判断是典型反模式——并发一高,两个请求同时查不到、同时插进去,重复就来了。
-
upsert: true是更贴近业务语义的选择:你真正要的是“这条中奖记录存在就不管,不存在才落库”,而不是“必须插入一条新文档” - 但前提是
filter字段必须有唯一索引支撑,否则多个upsert可能各自创建文档(比如只按uid过滤却没建索引) - 如果业务要求“绝对不允许覆盖已有记录”(比如中奖时间、IP 不能被后来同 uid 的请求改掉),那
Insert+ 唯一索引 +mgo.IsDup()或对应驱动的重复错误捕获才是正解
唯一索引建在哪?别只盯 uid
红包雨里光靠 uid 去重毫无意义——同一用户可中多次奖。真正需要约束的是“一次抽奖行为”的原子性,比如:{uid, activityId, prizeId, createTime} 组合,或更轻量的 {uid, activityId, traceId}(traceId 由前端/网关生成,保证单次点击唯一)。
- 索引字段顺序影响查询效率,把高频过滤字段放前面,比如
activityId通常比uid更早确定 - 别在
createTime上用精确时间戳做唯一键——毫秒级并发下仍可能冲突;改用带随机后缀的 traceId 更可靠 - 建索引用
Background: true,避免大集合上锁阻塞线上写入;且务必在服务启动时执行,不是每次插入前调
mgo.IsDup() 捕获失败,还是直接 panic?
Go 用 mgo 驱动时,Insert() 失败不等于数据错了——网络超时、权限不足、连接中断都会返回 err != nil。盲目当成重复处理,会把真实故障掩盖成“已存在”。
- 必须用
mgo.IsDup(err)显式判断,这是唯一安全识别唯一索引冲突的方式 - 其他错误类型该重试就重试(如网络类),该告警就告警(如权限类),不能一概
return nil - 如果用的是官方
mongo-go-driver,对应方法是检查writeexception.WriteError.Labels是否含"DuplicateKey"
异步写入下,怎么知道“到底写没写成”?
抽奖系统普遍走「MQ 异步落库」,消费者从 RabbitMQ 拿到消息后写 MongoDB。这时你没法在 HTTP 响应里告诉用户“已中奖并记账成功”,因为写库还没发生。
- 消息体里必须带完整上下文:traceId、uid、prizeId、客户端时间戳、IP、设备指纹——哪怕写库失败,也能靠这些字段人工对账
- 不要依赖 MongoDB 写入成功才发“中奖通知”,应该在扣减库存/生成中奖结果那一刻就发通知,写库只是归档;失败了走补偿任务重推
- 补偿逻辑里,依然要用
upsert或带唯一索引的Insert,否则重推两次又变两条记录










