redis.setnx是防重令牌的起点,因其具备“存在则不写入”的原子性,而set会无条件覆盖,无法保证幂等;正确用法是直接调用setnx并校验返回值true才执行业务逻辑。

为什么 redis.SetNX 是防重令牌的起点,而不是 redis.Set
因为 Set 没有原子性判断——它会无条件覆盖,而幂等的核心是“只接受第一次”。SetNX(set if not exists)才真正实现「写入前检查是否存在」的原子操作。用错就等于没防。
- 常见错误:先
GET再SET,中间存在竞态窗口,两个并发请求可能都通过检查 - 正确姿势:直接
redis.SetNX(ctx, tokenKey, "1", expireTime),返回true才继续业务逻辑 - 注意:Go 的
github.com/go-redis/redis/v9中,SetNX返回bool和error,别只看error就认为成功 - 兼容性提醒:Redis 6.2+ 支持
SET key value NX EX seconds单命令,v9 客户端已封装为SetNX,无需手拼命令
RPC 请求体里怎么塞 token 才不被绕过
不能只靠前端生成或拼在 URL 里——URL 可能被日志、代理、CDN 缓存,且无法验证来源合法性。token 必须由服务端签发、客户端透传、服务端校验闭环。
- 典型场景:支付回调、订单创建、优惠券领取这类「一次生效」操作
- 推荐做法:在 RPC 接口的
Requeststruct 里加一个IdempotencyKey string字段(不是 header),和业务参数一起走 gRPC 或 HTTP body - 容易踩的坑:
IdempotencyKey如果由前端生成(比如 UUID),攻击者可复用旧 key 重放;必须由调用方(如网关或 SDK)在首次请求时生成并缓存,后续重试复用同一 key - 性能影响:每次请求都查一次 Redis,但只要 key 设计合理(如
"idempotent:{service}:{method}:{key}"),单次SetNX耗时通常
redis.SetNX 成功后,业务失败了怎么办
SetNX 成功只代表「这是第一次请求」,不代表业务一定能成功。如果业务出错(比如 DB 写失败),这个 token 就卡住了,后续重试永远被拒——这反而破坏可用性。
- 必须配对使用:成功写入 token 后,业务执行完无论成败,都要用
DEL或带条件的EVAL清理 token,但仅限「业务确定失败且不可重试」时才删 - 更稳妥的做法:把 token 绑定到「最终状态」。例如,用
HSET idempotent:xxx status "processing",成功后改"success",失败后改"failed";重试时先查HGET,只有"processing"才阻塞,"success"直接返回结果,"failed"可选择重试或报错 - 注意 TTL:expire 时间得比最长业务耗时 + 网络抖动余量还长,否则 token 过期了,重试进来又当新请求——建议设为 5~15 分钟,别用固定 10s
gRPC 里怎么让幂等逻辑不侵入每个 handler
硬编码 SetNX 到每个 RPC 方法里,既重复又易漏。得靠拦截器(interceptor)统一收口,但关键是怎么传 token、怎么判重、怎么返回原结果。
立即学习“go语言免费学习笔记(深入)”;
- 必须提取 token:从
req.(interface{ GetIdempotencyKey() string })这类接口获取,而不是反射所有字段——反射慢且不稳定 - 拦截器里做三件事:生成唯一 key(含 service/method)、调
redis.SetNX、失败则查缓存结果并返回(需提前把成功响应序列化存 Redis) - 容易被忽略的点:gRPC 的
UnaryServerInterceptor拿不到 response body,所以「查到成功记录后返回原结果」这事得在 interceptor 里手动构造proto.Message并 return,不能靠后续 handler - 别忘了 context 超时:Redis 操作要带上原始 RPC 的
ctx,避免因 Redis 延迟拖垮整个调用链










