交易通知接口必须自己做幂等校验,因为http协议不保证通知只来一次,支付网关、消息队列和重试机制会导致同一笔交易的post /notify被重复推送;gin不自动处理,若无校验逻辑,将引发重复写库、发短信、扣库存等问题。

为什么交易通知接口必须自己做幂等校验
HTTP 协议不保证通知只来一次,支付网关、消息队列、重试机制都会导致同一笔交易的 POST /notify 被重复推送。Gin 本身完全不处理这个事——它只把请求转给你写的 handler,你没写校验逻辑,就一定会写多条记录、发多次短信、扣多次库存。
Idempotency-Key 必须由客户端生成并透传
不能服务端用 time.Now().UnixNano() 或 sha256(req.Body) 自动生成 key:前者在容器或高并发下极易重复,后者受 gzip、空格、字段顺序、代理改写干扰,同一通知可能算出不同哈希。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 前端/支付网关必须在请求头带
X-Idempotency-Key(推荐 UUID v4) - Gin 中间件用
c.GetHeader("X-Idempotency-Key")取值,为空直接返回400 Bad Request - 校验格式,比如正则
^[a-zA-Z0-9_-]{12,64}$,防 Redis key 注入 - key 构造必须含业务上下文:
"idempotent:pay_notify:" + userID + ":" + idempotencyKey,避免不同用户或不同交易复用
Redis SetNX 必须原子执行且带 EX 过期
常见错误是先 GET 再 SET,或 SETNX 成功后单独调 EXPIRE —— 中间若 panic 或网络断开,key 就永久滞留,后续所有合法重试都被拦截,等于服务不可用。
- 必须用原子命令:
rdb.Set(ctx, key, traceID, 30*time.Minute).AddArgs("NX", "EX", "1800") - 过期时间设为业务最长耗时 + 安全余量(如回调处理含第三方调用,设 30 分钟)
- value 别存空字符串,建议填
traceID或原始X-Idempotency-Key,方便日志对齐 - 如果
rdb.Set(...).Err() == redis.Nil,说明 key 已存在,应直接返回200 OK并附{"idempotent": true}
数据库唯一索引是兜底,不是可选项
Redis 可能宕机、网络分区、写入成功但业务 panic 导致未清理——单靠缓存永远无法 100% 保证幂等。DB 层必须建联合唯一索引,且冲突捕获要精准。
- 订单表加索引:
UNIQUE INDEX idx_user_id_notify_key (user_id, idempotency_key) - 插入失败后,不能吞掉所有 error;必须识别 PostgreSQL 的
pgx.ErrCodeUniqueViolation或 MySQL 的errno 1062 - 捕获到唯一冲突后,必须查一次 DB 确认该通知是否真已成功(防止“扣了库存但最后一步写库失败”的脏状态)
- 索引字段别太宽:
idempotency_key控制在 32 字符内,避免拖慢写性能
true,而是得存结构体 {status: "success", result: json.RawMessage, timestamp: time.Time},否则重试时无法原样返回响应体,前端可能因字段缺失报错。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










