接口幂等与防重提交怎么做

一句话结论:幂等指“同一个请求执行一次和多次,结果一致且不产生副作用”。落到实现上分四层:① 前端防抖(按钮置灰)只是体验优化,不可靠;② 后端幂等键(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 是分布式下的最终一致性防线。
  • 追问:失败请求要清掉幂等键吗?——业务失败应允许换键重试或保留“失败”状态让客户端明确重试,别让失败也永久幂等。
  • 记忆点:四层防线——前端防抖、幂等键串行、唯一索引兜底、条件更新防并发;重复返回首次结果而非再执行。
笔记加载中…