必须用redis+lua实现原子限流,因iris.limiter仅单机内存计数,多实例部署时失效;需用incr+expire原子脚本、带业务维度的key设计、合理连接池及降级策略。

直接用 iris.Limiter 做不了真正的分布式限流,它只在单实例内存里计数。要防刷、扛住 1.2 万 QPS 的抽奖请求,必须把计数逻辑落到 Redis 上。
为什么不能用 iris.Limiter + 本地内存
本地限流中间件在多实例部署时完全失效:用户 A 的第 3 次请求可能打到实例 1,第 4 次打到实例 2,两个实例各自计数,规则形同虚设。电商大促晚上八点那种场景,一上来就超限——不是代码写错了,是压根没走对地方。
真正能用的方案,是自己写一个基于 redis.Incr + redis.Expire 的原子计数器,并用 Lua 脚本保证“读-增-设过期”三步不被并发打断。
Redis 限流中间件的核心实现逻辑
关键不是“怎么写中间件”,而是“怎么让每次请求都原子性地完成:检查是否超限 → 计数 +1 → 设置/续期过期时间”。靠 Go 代码里先 GET 再 INCR 再 EXPIRE?不行,中间有竞态窗口。
- 必须用 Lua 脚本封装整个判断+操作,通过
redis.Eval一次性提交给 Redis 执行 - Key 设计要带业务维度,比如
rate:ip:192.168.1.100或rate:user:12345,不能只用固定字符串 - 脚本返回值必须是整型:0 表示拒绝,1 表示放行,Go 层据此调用
ctx.StopExecution() - 注意 Redis 连接池配置,
MaxActive至少设为 50+,否则限流本身成瓶颈
实际可用的中间件代码片段(Go + Iris v12)
假设你已初始化好 *redis.Client 实例叫 rdb:
func RedisRateLimiter(rdb *redis.Client, limit int, window time.Duration) iris.Handler {
script := redis.NewScript(`
local current = redis.call("INCR", KEYS[1])
if current == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[1])
end
if current > tonumber(ARGV[2]) then
return 0
end
return 1
`)
return func(ctx iris.Context) {
ip := ctx.RemoteAddr()
key := fmt.Sprintf("rate:ip:%s", ip)
ret, err := script.Run(rdb, []string{key}, window.Seconds(), limit).Int()
if err != nil || ret == 0 {
ctx.StatusCode(429)
ctx.WriteString("Too many requests")
ctx.StopExecution()
return
}
}
}
注册方式:app.Use(RedisRateLimiter(rdb, 100, 60*time.Second)) 表示每分钟最多 100 次请求。
容易踩的坑和线上注意事项
这个中间件看着简单,但上线后最容易出问题的地方不在逻辑,而在边界和运维:
- Key 没加前缀或没做清理,导致
rate:ip:...累积百万级 key,触发 Redis 内存淘汰甚至 OOM - 没对 IP 做标准化(比如忽略端口、处理代理头),真实用户被误限或攻击者轻松绕过
- Lua 脚本里用了
redis.call("TTL", ...)再判断是否重设过期,其实没必要——INCR对不存在 key 自动初始化为 1,只需首次EXPIRE - 没配
context.WithTimeout包裹 Redis 调用,网络抖动时整个 HTTP 请求卡死在限流环节 - 没预留降级开关,Redis 故障时不能自动 fallback 到本地计数(哪怕不准也比 500 强)
最常被忽略的一点:限流粒度要和业务规则对齐。比如「每人每天三次」,就不能只按 IP 限,得从 JWT token 解出 user_id,再拼成 rate:user:12345:20261013 ——日期必须参与 key 构造,否则跨天不清零。











