必须用Lua脚本实现原子限流,因INCR+EXPIRE非原子:若INCR后服务崩溃,key将永不过期;Lua在Redis端一次性执行自增与过期设置,彻底规避竞态。

直接用 Redis + Lua 脚本做原子限流,别碰中间件封装的“自动限流”组件——它们在高并发下容易漏判或误判。
为什么必须用 Lua 脚本而不是单纯 INCR + EXPIRE
Redis 的 INCR 和 EXPIRE 是两条独立命令,中间存在竞态窗口:如果服务刚执行完 INCR 就崩溃,EXPIRE 永远不会执行,key 就变成永不过期的僵尸计数器。
用 Lua 脚本能保证「自增 + 首次设置过期时间」这两步原子执行,这是防刷逻辑正确的底线。
- 脚本里只用
KEYS[1]和ARGV[1],避免硬编码 key 名或过期秒数 - 不要在 Lua 里写条件判断返回布尔值再由 Go 判断——网络往返延迟会放大误差,直接让 Lua 返回当前计数值,Go 层只比对
count > limit - 注意
window.Seconds()是 float64,Lua 的redis.call("EXPIRE", ...)只接受整数,得用math.floor(window.Seconds())或提前转 int
key 设计要带路径和 IP,但别裸露用户标识
示例中 key := fmt.Sprintf("rate:%s:%s", ctx.Path(), ip) 是合理起点,但要注意:
- 如果接口带查询参数(如
/api/v1/user?tab=history),直接拼全路径会导致缓存碎片化;建议统一截取到问号前,或用ctx.Request().URL.EscapedPath()避免编码差异 - 别把
ctx.GetUser().ID直接塞进 key——未登录用户会 panic,已登录但 token 过期时也可能拿不到 ID;IP 是最稳妥的兜底维度 - 若需用户级限流,建议单独走另一套 key:
user:rate:,并配合 Redis 的: SCAN做后台清理,别和 IP 级混在一个命名空间
429 响应要带 Retry-After,且别依赖框架默认行为
Iris 的 ctx.StatusCode(429) 不会自动加 Retry-After 头,前端重试逻辑会失去依据。
- 必须手动写:
ctx.Header("Retry-After", strconv.FormatInt(int64(window.Seconds()), 10)) - 如果 window 是
time.Minute * 5,别传 300 秒——有些前端库只认整数秒,小数会解析失败 - 不要用
ctx.Next()后再统一拦截写头,限流中间件必须在判定失败后立即写头并ctx.StopExecution(),否则可能被后续中间件覆盖状态码
压测时最容易被忽略的点:连接复用导致 IP 误判
本地用 curl 或 Postman 测没问题,一上 K8s 或 Nginx 就发现限流失效——大概率是反向代理没透传真实 IP。
- 检查
ctx.RemoteAddr()拿到的是不是10.x.x.x或127.0.0.1;如果是,说明你没配X-Real-IP或X-Forwarded-For解析 - Iris 默认不解析这些 header,得自己加一层:
ip := ctx.GetHeader("X-Real-IP"),为空再 fallback 到ctx.RemoteAddr() - Redis key 中的 IP 如果是内网地址,所有请求都会挤在同一个 key 下,看起来像“全站被封”,其实是代理层没配置好
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











