必须用 set key value nx ex seconds 原子命令,因 setnx+expire 非原子:若 setnx 成功后 expire 失败(如崩溃、网络中断),key 永久残留导致死锁,引发服务雪崩。

直接说结论:用 SET key value EX seconds NX 原子命令,不是 SETNX + EXPIRE 两步,也不是 INCR 或 GETSET。 否则大概率线上出问题——Key 永久残留、误拒合法请求、并发下漏判,三选一几乎必中。
为什么不能用 SETNX + EXPIRE 两步走
看似逻辑清晰:先 SETNX 判断是否存在,再 EXPIRE 设过期。但中间存在竞态窗口:
- 若
SETNX成功、EXPIRE因网络中断/panic/超时失败,key 就永久存在,后续所有同 key 请求全被拦截,服务等效雪崩 - Redis 客户端(如
github.com/go-redis/redis/v8)的SetNX方法默认不带 TTL,必须手动补Expire,极易遗漏 - 旧版 Redis(SET ... NX EX,但当前生产环境基本都已升级,没必要倒退兼容
正确姿势是:用现代客户端的 Set(ctx, key, value, ttl),它底层自动拼装 SET key value EX seconds NX 命令(github.com/redis/go-redis/v9 默认启用 NX 行为)。
SET 命令里 value 存什么、EX 设多久
value 不是占位符,得有业务意义;EX 时间不是拍脑袋,得覆盖真实链路耗时:
-
value推荐存客户端传的X-Idempotency-Key或服务端生成的 trace_id,便于日志串联和人工排查;不要存空字符串或"1" -
EX必须 ≥ 接口最长可能耗时(含 DB 超时、远程调用、重试),建议设为超时时间的 1.5–2 倍;例如 HTTP 超时 30s,TTL 至少设 45s;支付类接口可能卡住 1 小时,那就设3600 - 绝对别用
time.Now().Unix()动态算 TTL —— 系统时间跳变、容器时钟漂移会导致 key 提前过期或永不过期
Key 拼接必须带业务上下文,不能只靠客户端 token
仅用前端传的 X-Idempotency-Key 当 key,会撞坑:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 不同用户、不同接口共用同一 token 时,key 冲突,A 用户的提交被 B 用户的请求误拦
- 攻击者可复用历史 token 发起跨业务重放(比如拿登录 token 提交订单)
安全拼法是:"idempotent:" + userID + ":" + operation + ":" + idempotencyKey,例如:
idempotent:u_789:order_create:abc123-def456
其中 operation 是固定字符串(如 "order_create"),不是动态路由路径(防注入);userID 必须来自可信来源(如 JWT payload 或 session),不能取自请求参数。
Redis 失败时不能 fallback 到“放行”,必须明确拒绝
缓存层不可用 ≠ 业务可降级。如果 SET 返回连接错误、超时或 redis.Nil(表示已存在),处理逻辑完全不同:
- 返回
redis.Nil→ 已存在 → HTTP 状态码返回409 Conflict,body 带{"code": "IDEMPOTENT_CONFLICT"} - 返回连接错误/超时 → Redis 不可用 → HTTP 状态码返回
503 Service Unavailable,并记录告警;绝不能静默放行,否则幂等性彻底失效 - Go 客户端如
go-redis/v9的Set方法返回err != nil时,需显式判断是否为redis.Nil(用errors.Is(err, redis.Nil)),其余错误统一按 503 处理
最容易被忽略的是:Redis 故障时的兜底策略不是技术问题,而是业务契约问题——你承诺了幂等,就不能因缓存不可用而违约。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










