接口幂等与防重提交怎么做
一句话结论:幂等指“同一个请求执行一次和多次,结果一致且不产生副作用”。落到实现上分四层:① 前端防抖(按钮置灰)只是体验优化,不可靠;② 后端幂等键(Idempotency-Key)——Redis SETNX 记录处理状态并缓存结果,同键重复请求直接返回首次结果;③ DB 唯一约束是最终防线(订单号/流水号建唯一索引,重复插入即“已存在”);④ 状态机 + 条件更新(UPDATE ... WHERE status='pending')防并发重复扣款。防重提交本质是“唯一键判重 + 状态流转只允许一次”。
手段对比
| 手段 | 防什么 | 局限 |
|---|---|---|
| 前端按钮置灰/令牌 | 正常人双击 | 可绕过,非安全边界 |
| Redis 幂等键(SETNX+结果缓存) | 重试、重复提交 | 键要设 TTL;Redis 非事实源 |
| DB 唯一索引 | 一切重复写 | 需业务唯一键;冲突要翻译成友好错误 |
| 条件 UPDATE/乐观锁 | 并发重复扣款/改单 | 依赖状态设计正确 |
幂等键示例(Redis)
key := fmt.Sprintf("idem:%d:%s", uid, c.GetHeader("Idempotency-Key"))
ok, err := rdb.SetNX(ctx, key, "processing", 10*time.Minute).Result()
if !ok { // 已存在:返回缓存的处理结果或“处理中”
resp.Replay(c, rdb.Get(ctx, key).Result())
return
}
result := doBiz() // 执行业务
rdb.Set(ctx, key, marshal(result), ttl) // 存结果供重放
注意:Redis 只保证“同键请求串行化”,真正防重复入库靠 DB 唯一索引兜底;键要绑定用户+动作,防止跨用户重放。
防重复扣款(DB 条件更新)
res := db.Model(&Order{}).
Where("id = ? AND status = 'pending'", orderID).
Update("status", "paid").RowsAffected
if res == 0 { return ErrAlreadyProcessed } // 已处理过,幂等返回
防重提交 token 变体
提交前向服务端取一次性 token(存 Redis),提交时用 Lua 原子“取走并删除”,谁拿到谁成功——必须原子,先 GET 后 DEL 会有并发漏洞。
追问记忆点
- 追问:为什么幂等键要设 TTL?——防键无限膨胀和客户端忘带键;过期后极端重放靠 DB 约束兜底。
- 追问:Redis 锁/键和 DB 唯一索引都要吗?——要:Redis 挡并发双击与重试风暴,DB 是分布式下的最终一致性防线。
- 追问:失败请求要清掉幂等键吗?——业务失败应允许换键重试或保留“失败”状态让客户端明确重试,别让失败也永久幂等。
- 记忆点:四层防线——前端防抖、幂等键串行、唯一索引兜底、条件更新防并发;重复返回首次结果而非再执行。