支付回调必须做幂等校验,因为微信、支付宝等平台通知可能重复2~5次且跨分钟到达;需用redis原子锁(set nx)结合out_trade_no+transaction_id防重,过期设300秒,并在中间件统一拦截,锁后还需查库确认订单状态以防故障导致的重复处理。

为什么支付回调必须做幂等校验
因为第三方支付平台(如微信、支付宝)在通知失败时会重试,且不保证只推送一次——你收到的 POST /notify 可能重复 2~5 次,甚至跨分钟到达。Gin 默认不拦截重复请求,靠前端防重或网络层去重完全无效。
核心判断依据是:同一笔订单的 out_trade_no + transaction_id 组合,在业务逻辑中只能成功处理一次。
Gin 中用 Redis 实现原子性幂等锁
别用内存 map 或本地缓存,多实例部署下会失效;也别依赖数据库唯一索引硬扛——高并发时主键冲突抛异常,既影响性能又污染日志。
- 用
SET key value EX 300 NX命令尝试写入 Redis,返回1表示首次请求,可继续处理;返回nil表示已存在,直接返回成功响应(HTTP 200) - key 建议拼成
"pay:dedup:" + outTradeNo + ":" + transactionId,避免不同渠道 ID 冲突 - 过期时间设为 300 秒(5 分钟),覆盖支付平台最长重试窗口,同时防止锁永久残留
- 务必在业务逻辑执行前加锁,且锁的 value 设为随机 UUID(如
uuid.New().String()),便于后续排查
在 Gin 中间件里统一拦截重复通知
把幂等校验下沉到中间件,避免每个 POST /notify handler 里重复写逻辑,也防止漏掉。
- 中间件需先解析请求体(
c.Request.Body),提取out_trade_no和transaction_id字段(注意微信用 XML、支付宝用 JSON,需分别处理) - 校验失败时,直接调用
c.AbortWithStatus(200)并写入固定响应(如"success"),不要返回错误码——否则支付平台会持续重试 - Redis 连接建议用连接池(
redis.NewClient),并设置Context超时(例如context.WithTimeout(ctx, 500*time.Millisecond)),避免锁服务不可用拖垮整个通知接口
幂等校验后还要再查一次订单状态
加锁只解决“不重复执行”,不等于“业务一定没成功”。比如第一次处理卡在更新数据库前崩溃,锁自动释放,第二次请求进来仍会走完整流程——但此时订单可能已在 DB 中完成,导致重复发货或扣款。
所以必须在锁通过后,立刻查库确认该订单是否已是 paid 状态:
- 查不到订单 → 正常创建并更新
- 查到且状态为
paid→ 直接返回成功,不触发下游动作 - 查到但状态为
processing→ 可选择等待或主动轮询最终状态,避免死锁
这个二次校验不能省,它和 Redis 锁是两道不同防线:锁防并发,DB 查防故障恢复后的脏状态。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











