数据库唯一索引仅能兜底,无法拦截并发请求同时闯入业务逻辑;必须用redis setnx(带nx和ex)在gin入口前置校验,并拼接userid等上下文构造幂等key,redis故障时须返回503而非放行。

为什么 Gin 里不能只靠数据库唯一索引防重复
唯一索引只能兜底,拦不住并发请求同时闯入业务逻辑。两个 goroutine 几乎同时执行 SELECT 判重、再 INSERT,其中至少一个会触发 mysql.ErrDupEntry 或 pgx.ErrCodeUniqueViolation,但此时第一笔订单可能已扣款、发消息、更新库存——状态已经不一致了。
更麻烦的是:日志里看到相同 X-Idempotency-Key 出现两次,说明服务端根本没在入口拦截,而是放行到 DB 层才冲突。这不是幂等,是“撞库”。
用 go-redis/v9 的 SetNX 做前置拦截必须带 EX 和 NX
别手写 SETNX + EXPIRE 两步命令,中间崩溃会导致 key 永久残留;也别漏掉 NX 参数,否则每次请求都覆盖旧值,完全失去去重意义。
正确姿势是直接调用:rdb.SetNX(ctx, key, value, ttl)(v9 自动拼 NX EX),value 推荐填客户端传的 X-Idempotency-Key 或 trace_id,方便日志对齐。ttl 必须大于接口最长耗时 + 安全余量(比如支付类设 3600 秒),太短会误判重试,太长堆积无效 key。
Gin 中间件里构造幂等 key 不能只用 Header
纯靠 c.Request.Header.Get("X-Idempotency-Key") 作 key 是危险的:不同用户、不同订单复用同一 key,会误拒或漏判。
必须拼业务上下文,例如:"idempotent:pay:" + userID + ":" + idempotencyKey。
还要做三件事:
- X-Idempotency-Key 为空时直接返回 400,不默认生成
- 正则校验格式(如 ^[a-zA-Z0-9_-]{12,64}$),防 Redis key 注入
- 别哈希整个 request body,顺序、空格、gzip 编码都会导致不一致
Redis 故障时不能 fallback 到“放行”,得返回 503
Redis 连接失败、命令超时、集群不可用——这些都不是“可以忽略”的异常。一旦放行,幂等语义就崩了,重复请求直接打穿 DB。
必须显式判断:if err != nil 时返回 http.StatusServiceUnavailable(503),并记录 ERROR 日志。
DB 唯一索引仍是铁律,但只用于兜底:插入失败后要捕获具体错误码,再 SELECT 确认是否真已存在,不能静默吞掉 ErrDupEntry 就返回 success。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











