首页/数据/NoSQL 速查/幂等设计:去重键与唯一索引
选型与架构50 条

幂等设计:去重键与唯一索引

至少一次投递、用户重复点击、网络重试都会带来重复请求。幂等实现分两层:请求层用幂等键加唯一索引拦截重复提交;数据层用 upsert 与版本条件让重复执行不产生额外副作用。两层结合才能覆盖乱序与并发。

NoSQL 速查数据更新于 2026-10-13

幂等要解决什么

至少一次投递、用户重复点击、网络重试都会带来重复请求。幂等设计分两层:请求层用幂等键加唯一索引拦住重复提交,数据层用 upsert 与版本条件让重复执行不产生额外副作用。只做一层都不完整——请求层挡不住并发,数据层挡不住重复的副作用类操作(如发通知、扣减库存)。

唯一索引拦截重复提交(MongoDB 示例)

唯一索引拦截重复提交(MongoDB 示例)
// 每个业务请求带一个幂等键(客户端生成或网关下发)
db.idempotency.createIndex({ key: 1 }, { unique: true })

try {
  db.idempotency.insertOne({ key: "req-8f2c", createdAt: new Date() })
  // 首次请求:继续执行业务逻辑
} catch (e) {
  if (e.code === 11000) {
    // 重复请求:查询首次结果直接返回,或告知处理中
  } else {
    throw e
  }
}

// 清理:给幂等键设 TTL,覆盖业务允许的重试窗口即可
db.idempotency.createIndex({ createdAt: 1 }, { expireAfterSeconds: 86400 * 7 })

基本信息

请求层幂等键 + 唯一索引:同一业务请求只允许成功登记一次
数据层按主键 upsert:重复执行结果一致;带版本条件更新则让旧版本重放不生效
消息层按事件 ID 去重:消费端记录已处理事件,重复投递直接跳过
窗口设计去重记录的保留时长要覆盖该接口的重试与重放周期
检查方式把同一请求重复提交两次,比较数据与副作用是否只发生一次

典型使用场景

  • 支付与下单:用户重复提交时只能产生一笔业务
  • 消息消费:队列重投时处理结果不变,不重复扣减或发货
  • 导入任务:失败重跑整批任务时不会重复写入
  • 外部回调:第三方多次回调同一事件只处理一次
  • 定时对账:任务重跑的结果与首次执行一致

实现层次对照

层次手段覆盖的重复来源
请求层幂等键 + 唯一索引用户重复点击、客户端超时重试
数据层按主键 upsert、版本条件更新消息重投、任务重跑、并发写入
消息层事件 ID 去重表至少一次投递带来的重复事件
外部副作用先登记后执行、状态机约束重复调用下游接口
可观测去重命中计数发现上游重试异常与客户端缺陷
幂等窗口要覆盖该接口的重试与重放周期:窗口过短,晚到的重试会重复执行;窗口过长,去重表持续膨胀,需要配合清理策略。
先查再插的写法在并发下不可靠:两个请求可能同时查不到再同时插入,只有唯一索引才能真正挡住重复。

易错点

  • 用「先查询是否存在再插入」做去重:并发下两个请求同时查不到再同时插入,必须依靠唯一索引兜底
  • 幂等键由服务端每次生成:同一个业务请求每次重试都换新键,等于没有幂等
  • 只对数据库写入做幂等:发通知、调用外部接口等副作用仍会重复产生
  • 去重记录长期不清理:表持续膨胀,索引与存储成本上升,最终影响登记性能

数据来源与参考

  • MongoDB Manual: Unique indexes
  • Elasticsearch Guide: Index API(同 ID 覆盖语义)
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌