根本原因是业务逻辑未做幂等校验,而非HTTP协议问题;需用Redis SetNX基于请求指纹(如user_id+参数哈希)实现前置校验,并绑定context透传、设置过期、失败清理。

为什么 POST /orders 重复提交会创建多笔订单
根本原因不是 HTTP 协议本身不支持幂等,而是业务逻辑没做校验。HTTP 的 POST 方法语义上就是非幂等的,浏览器刷新、网络重试、用户双击都可能触发多次请求,而 Go 的 http.ServeMux 或 gin.Engine 默认完全不管这个事——它只管把请求转给你的 handler,至于你写不写去重逻辑,它不拦着也不提醒。
常见错误现象:200 OK 返回了,但数据库里冒出两条一模一样的订单;日志里看到相同 X-Request-ID 出现两次,但业务层毫无感知。
- 别依赖前端加防抖:JS 防抖拦不住网络层重传或代理重发
- 别只靠数据库唯一索引硬扛:冲突报错
ERROR: duplicate key value violates unique constraint属于事后补救,用户体验差,还可能掩盖真实并发问题 - 幂等校验必须在业务逻辑执行前完成,且要覆盖整个关键路径(比如库存扣减 + 订单创建不能拆成两步校验)
用 redis.SetNX 实现请求指纹锁
最常用也最稳妥的方式:把请求的业务标识(比如用户 ID + 订单参数哈希)作为 key,用 Redis 的原子操作 SETNX 上锁。成功则放行,失败则直接返回 409 Conflict 或缓存中的结果。
关键点在于“指纹”怎么设计:
立即学习“go语言免费学习笔记(深入)”;
- 推荐组合:
idempotent:+user_id+sha256(order_amount + goods_id + timestamp)—— 不要裸用客户端传的Idempotency-Key,它可能被恶意复用或伪造 -
SETNX必须配EX过期时间(如300秒),否则服务崩溃后锁永远不释放 - 如果业务允许返回历史结果(比如查单),得额外用一个
GET+SET双读写保证:先查是否存在结果,再尝试写入锁,避免竞态
示例片段(基于 github.com/go-redis/redis/v9):
key := fmt.Sprintf("idempotent:%d:%s", userID, hashParams(req))
ok, err := rdb.SetNX(ctx, key, "processing", 5*time.Minute).Result()
if err != nil {
// redis 故障,降级为非幂等处理(需记录告警)
return
}
if !ok {
// 已存在处理中,返回 409 或查缓存结果
return
}
中间件里怎么安全地注入幂等上下文
Go Web 框架(gin、echo、原生 net/http)中间件本身不带状态穿透能力,不能靠闭包变量存 idempotent_key —— 并发请求会互相污染。
正确做法是把幂等标识绑定到 context.Context,再透传给后续 handler:
- 从请求头取
Idempotency-Key时,务必校验格式(如正则^[a-zA-Z0-9_-]{12,64}$),防止 Redis key 注入 - 不要在中间件里直接调用业务 DB 或 RPC:中间件职责只是“判重+放行”,业务逻辑必须在 handler 里执行
- 若 handler 执行失败(panic / error),中间件应主动删掉 Redis key(用
defer rdb.Del(...)),否则下次同 key 请求会被误判为“已处理”
典型结构:
func IdempotentMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
key := c.GetHeader("Idempotency-Key")
if key == "" {
c.AbortWithStatusJSON(400, gin.H{"error": "missing Idempotency-Key"})
return
}
ctx := context.WithValue(c.Request.Context(), "idempotent_key", key)
c.Request = c.Request.WithContext(ctx)
c.Next()
}
}
哪些场景下 redis.SetNX 会失效
不是所有情况都适合用 Redis 锁。以下几种容易踩坑:
- 高 QPS 下 Redis 成为瓶颈:单实例 Redis 的
SETNX在 10w+ QPS 时延迟明显上升,要考虑分片或改用本地缓存(但要接受节点间不一致) - 跨地域部署时网络延迟大:比如新加坡节点写 Redis 在东京,RTT 80ms,会拖慢所有幂等请求
- 事务型操作无法回滚:比如你用
SetNX放行后,在 DB 插入失败,Redis key 还在,此时用户重试会直接返回“已存在”,但实际没成功——必须配合最终一致性补偿(如定时扫描过期未完成的 key) - 文件上传类接口:
multipart/form-data请求体太大,不适合哈希进 key,得改用预签名 URL + 上传 ID 做幂等
真正难的不是写对那几行 SetNX,而是想清楚:这个请求失败后,用户重试时该返回什么?是空响应、上次结果,还是明确告知“正在处理中”?这决定了幂等策略的粒度和存储成本。










