广告计费写入必须幂等,因同一曝光/点击可能被重复上报,直接insertone会导致计费翻倍;需用ad_event_id唯一索引+insertone捕获duplicatekeyerror来保障;汇总须用findoneandupdate原子更新,避免先查后改引发竞态。

广告计费写入为什么必须幂等?
因为广告系统里,同一笔曝光/点击可能被多次上报(重试、网络抖动、SDK重复触发),如果每次写入都直接 insertOne,就会导致计费翻倍。MongoDB 本身不提供“插入前查重再插”的原子性,所以得靠业务层控制——最稳妥的方式是用带唯一约束的字段做幂等键,比如 ad_event_id(由客户端生成 UUID 或服务端拼接 ad_id + timestamp + seq)。
实操建议:
- 在集合上对
ad_event_id建唯一索引:db.ad_events.createIndex({ ad_event_id: 1 }, { unique: true }) - 写入统一用
insertOne,捕获DuplicateKeyError(驱动里通常是MongoWriteException或具体错误码11000),遇到就忽略或记录为“已存在” - 避免用
updateOne({ ad_event_id: xxx }, { $setOnInsert: {...} }, { upsert: true })——它看似幂等,但若匹配条件宽松(比如只按ad_id查),会导致不同事件覆盖彼此
计费汇总怎么保证原子性不丢不重?
广告计费不是单条写完就结束,还得实时累加到计划/创意/账户维度的余额或消耗统计里。MongoDB 的 findAndModify 或 findOneAndUpdate 是唯一能在一个操作里“读+改+写”的原子路径,但要注意:不能先 find 再 update,那中间可能被其他请求修改。
实操建议:
- 对每个需汇总的维度(如
campaign_id),用findOneAndUpdate直接更新消耗字段:db.campaigns.findOneAndUpdate({ _id: campaignId }, { $inc: { spent: 0.35 } }, { returnDocument: 'after' }) - 别依赖返回值做二次判断——比如“如果返回 spent
- 如果要同时更新多个字段(如
spent、impression_count、last_active_time),全塞进一个$inc和$set里,不要拆成两次调用
为什么不能用事务替代幂等+原子更新?
MongoDB 事务(4.0+)确实能包住 insert + update,但广告场景下它反而增加风险:事务有默认 60 秒超时,高并发写入容易卡住;且跨分片事务性能差、运维复杂。更重要的是——事务解决不了“重复请求”问题,它只保证“这一批操作要么全成功要么全失败”,而重复请求是两个独立事务。
实操建议:
- 单文档内更新(如更新 campaign 消耗)坚决不用事务,
findOneAndUpdate足够 - 跨文档强一致性需求(如扣款+发通知)才考虑事务,且必须配
readConcern: "majority"和显式session.startTransaction() - 所有事务块内禁止调用外部 API 或长耗时逻辑,否则极易触发超时回滚,留下半截状态
聚合查询消耗数据时容易漏掉什么?
报表或对账常从原始事件表 ad_events 聚合 sum(spent),但若写入用了幂等设计,count() 和 $sum 就天然准确;可一旦中间加了归档、TTL 删除或分库分表,就可能漏数据。
实操建议:
- 不要依赖聚合结果做实时扣费,只用于离线核对;实时计费必须走前面说的原子更新字段
- 加 TTL 索引前确认:过期时间是否覆盖了最长对账周期?例如财务对账要拉 90 天数据,TTL 却设成 30 天,就会丢数
- 如果用了 change stream 做异步汇总,记得在 pipeline 里过滤
operationType: "insert",避免 delete/update 事件干扰
幂等键的设计和原子更新的粒度选择,比用不用事务更影响准确性——很多人卡在“以为事务万能”,其实把 ad_event_id 生成规则搞错,或者汇总字段没加唯一索引,后面全白搭。











