直接用 fiber.Ctx.Locals 存 token 校验结果不行,因为 Locals 仅在单次请求生命周期内有效,无法跨请求共享状态;幂等性要求同一 idempotency-key 的多次请求只执行一次业务,必须依赖 Redis 等分布式缓存或带唯一约束的数据库实现全局判重。

为什么直接用 fiber.Ctx.Locals 存 token 校验结果不行
因为 Locals 只在单次请求生命周期内有效,无法跨请求共享状态。幂等性要求“同一 idempotency-key 的多次请求只执行一次业务”,必须依赖外部存储做全局判重。常见错误是把校验逻辑写成中间件但只查内存 map 或局部变量,上线后并发一高就失效。
真正可行的路径只有两条:带过期时间的分布式缓存(推荐 redis),或带唯一约束的数据库(适合低频、强一致性场景)。Fiber 本身不提供幂等性原语,得自己串起「接收 key → 查缓存 → 执行业务 → 写缓存」这一链路。
redis.SetNX + redis.Expire 是最简可靠组合
不要用 redis.Set 加 EX 参数一步到位——某些旧版 Redis 客户端(如 go-redis/redis/v8 低于 v8.11)对原子性支持不一致;也不要分开调 SetNX 和 Expire,中间可能被中断导致 key 永久残留。
- 用
redis.SetNX(ctx, key, value, ttl)原子写入,返回true表示首次提交成功 - 若返回
false,说明该idempotency-key已存在,直接返回409 Conflict或复用前次结果(需额外存响应体) -
ttl建议设为业务最大处理时长的 2–3 倍,比如下单接口最长耗时 5s,设15 * time.Second - key 命名建议含服务名和接口路径,例如
"order:create:POST:/v1/orders:" + idempotencyKey,避免不同接口 key 冲突
前端怎么传 Idempotency-Key 才不踩坑
HTTP 规范没强制要求 header 名,但业界通用 Idempotency-Key(注意大小写和中划线)。Fiber 默认不自动解析这个 header,必须手动取:
key := c.Get("Idempotency-Key")
if key == "" {
return c.Status(400).SendString("missing Idempotency-Key header")
}
容易被忽略的点:
- 前端生成 key 不能用时间戳或随机 UUID —— 页面刷新后就变了,用户点两次仍是不同 key;应基于表单内容哈希(如
sha256(orderJSON))或由前端持久化存储(localStorage/sessionStorage)复用 - 后端要 trim 空格、限制长度(比如 ≤ 64 字符),防止恶意超长 key 打满 redis 内存
- 如果接口支持重试,前端应在 retry 时透传原始
Idempotency-Key,而不是每次重试都新生成
失败重试时怎么返回上次结果而不是空响应
单纯拦截重复请求返回 409 对用户体验不友好。更合理的是:首次成功后把响应体也存进 redis,后续重复请求直接 c.Status(200).JSON(storedResp)。
但要注意:
- 只存成功响应(
status >= 200 && status ),失败响应不缓存,否则错误状态会被固定返回 - 响应体不宜过大,建议上限 1MB;超过则只存关键字段(如
order_id,status),业务层再查库补全 - redis key 和响应体 key 要分离,例如用
idempotent:resp:{key}存 body,idempotent:lock:{key}做锁,避免大 value 影响锁操作性能 - 别忘了设置响应 header:
c.Set("X-Idempotent-Response", "true"),让前端知道这是复用结果
真正的难点不在代码几行,而在于 redis 故障时的降级策略——比如是否允许 fallback 到本地内存缓存(仅限单机部署)、还是直接拒绝并提示“服务暂不支持重复提交”。这个决策没法交给框架,得结合你的可用性要求来定。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











