幂等要解决什么
至少一次投递、用户重复点击、网络重试都会带来重复请求。幂等设计分两层:请求层用幂等键加唯一索引拦住重复提交,数据层用 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 去重表 | 至少一次投递带来的重复事件 |
| 外部副作用 | 先登记后执行、状态机约束 | 重复调用下游接口 |
| 可观测 | 去重命中计数 | 发现上游重试异常与客户端缺陷 |
幂等窗口要覆盖该接口的重试与重放周期:窗口过短,晚到的重试会重复执行;窗口过长,去重表持续膨胀,需要配合清理策略。
先查再插的写法在并发下不可靠:两个请求可能同时查不到再同时插入,只有唯一索引才能真正挡住重复。