应安全提取Idempotency-Key:先统一转小写获取,校验长度16–64且匹配^[a-zA-Z0-9_-]{16,64}$,空或不合规则返回400;再用go-redis/v9的rdb.SetNX原子写入,key拼接method、path、user_id和key,value填trace_id,ok==false为正常拦截,err!=nil时返回503。

怎么从 Echo 请求里安全提取 Idempotency-Key
直接 c.Request.Header.Get("Idempotency-Key") 会出事:空值、超长(比如 512 字符)、含控制字符(\x00)、甚至多个同名 header 被拼接成乱码。必须前置过滤。
- 为空或长度不在 16–64 字符范围内,直接返回
400 Bad Request - 用正则
^[a-zA-Z0-9_-]{16,64}$校验,不匹配就拒掉 - 别信任
http.CanonicalHeaderKey自动转换——idempotency-key和IDEMPOTENCY-KEY都得识别,但最终统一转小写再取值 - 取到后立刻存进
c.Set("idempotency_key", key),后续中间件或 handler 通过c.GetString("idempotency_key")读,避免重复解析
如何用 go-redis/v9 做原子幂等占位
别写 rdb.Exists(ctx, key) 再 rdb.Set(ctx, key, "1", ttl) —— 这是竞态漏洞。两个请求几乎同时执行,都会看到不存在,然后都写入,幂等失效。
- 必须用
rdb.SetNX(ctx, key, traceID, 30*time.Second),它底层自动拼SET key value EX 30 NX原子命令 -
key推荐格式:"idempotent:POST:/orders:user_123:" + idempotencyKey,绑定 method、path、user_id,防跨接口/用户冲突 -
value别填空字符串,填c.GetString("trace_id")或原始idempotencyKey,方便日志对齐 - 如果
ok == false(即 key 已存在),不是错误,是正常拦截;只有err != nil才要记录告警并考虑降级
Redis 不可用时该怎么响应
不能 fallback 到“放行”,那等于放弃幂等语义。支付、下单类接口一旦双写,后果是资金或库存错误,不是 500 能掩盖的。
- Redis 连接失败、超时、集群节点失联,一律返回
503 Service Unavailable,带X-Retry-After: 1提示客户端稍后重试 - 不要静默跳过校验——哪怕只有一台实例 Redis 挂了,也不能让这台机器变成“非幂等孤岛”
- 若业务允许临时降级(如内部管理接口),也必须走开关控制(如
config.IdempotencyEnabled),且开启审计日志 - 注意:go-redis 的
ctx超时要设短(建议 ≤3s),避免阻塞整个请求链路
为什么缓存结果不能只存个 bool
只存 rdb.SetNX(...) 成功就认为“已处理”,会导致两个问题:无法区分“正在执行中”和“已成功”,也无法返回上次完整响应体。用户重试时只能收到 409,而不是原样结果。
立即学习“go语言免费学习笔记(深入)”;
- 推荐结构体缓存:
{"status":"success","result":json.RawMessage,"ts":1725223200},用rdb.Set(ctx, key+":result", payload, 24*time.Hour) - 首次写入时先
SetNX占位为"pending",业务成功后再Set结果 + 更新状态为"success" - 当二次请求命中
key且状态为"success",直接json.Unmarshal并c.JSON(status, result)返回 - 别用
sync.Map存响应体——它不跨实例,服务重启即丢,且没 TTL,容易内存泄漏
SetNX,而是把 idempotency_key 从 header 里抠出来之后,始终让它和 user_id、method、path 绑定在一起参与计算,且在 Redis 故障时敢返回 503 而不心虚。


















