限流必须在控制器外层(路由分组或全局中间件)实现,MVC层不拦截中间件链;控制器内写计数逻辑无效,因无法阻断执行、无并发安全且绕过限流机制。

限流必须在控制器外层做,MVC层不拦截中间件链
直接在控制器方法里写计数逻辑是无效的——Iris 的 MVC 不会自动把中间件注入到每个 controller 方法执行前。限流必须放在路由分组(Party)或全局中间件里,否则请求根本不会被拦住。
常见错误现象:在 GetByID 方法开头加 if count > 10 { ctx.StatusCode(429); return },结果压测时照样打穿服务。因为这属于业务逻辑分支,不是限流,更无法阻断后续中间件或参数绑定过程。
- 限流中间件需注册在
app.Party("/api")上,且必须在mvc.Configure之前调用.Use() - 不能靠 controller 字段存计数器(如
type UserController struct { counter map[string]int }),没有并发安全保证,也做不到跨实例共享 - 若用内存计数(如
sync.Map),仅适合单机调试;生产环境必须用 Redis 或 token bucket 等外部存储
用 redis + golang.org/x/time/rate 实现分布式令牌桶
推荐组合:redis 存储窗口状态 + golang.org/x/time/rate 的 Limiter 做客户端侧预判(非必须),但核心判断必须走 Redis Lua 脚本,避免竞态。
示例中间件(注册在 Party 上):
func RateLimitMiddleware(redisClient *redis.Client, limit int, window time.Duration) iris.Handler {
return func(ctx iris.Context) {
ip := ctx.RemoteAddr()
key := fmt.Sprintf("rate:%s:%s", ctx.Path(), ip)
// Lua 脚本原子执行:incr + expire + 判断
script := redis.NewScript(`
local current = redis.call("INCR", KEYS[1])
if current == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[1])
end
return current
`)
count, err := script.Run(ctx, redisClient, []string{key}, window.Seconds()).Int()
if err != nil || count > limit {
ctx.StatusCode(429)
ctx.WriteString("Too many requests")
ctx.StopExecution()
return
}
ctx.Next()
}
}
-
ctx.StopExecution()必须调用,否则即使返回 429,MVC 控制器仍会继续执行 - 脚本里用
INCR+EXPIRE组合,比SET key val EX seconds NX更可靠:避免窗口重置失败 - key 中拼入
ctx.Path()可实现接口级限流;若要去掉路径,只按 IP 限流,删掉即可
控制器内怎么配合限流做友好提示?
限流中间件只负责拦请求、返状态码,但用户需要知道“还能等多久”。这时控制器可以读取限流中间件写入的上下文信息(需手动透传),或查 Redis 获取剩余配额。
- 不要在控制器里重复实现限流逻辑,那会绕过中间件,导致 double-check 和性能浪费
- 如果必须返回剩余次数,建议限流中间件把
X-RateLimit-Remaining写进ctx.Values().Set(),控制器再取:remaining := ctx.Values().Get("rate_limit_remaining") - 注意:Iris 的
ctx.Values()是 per-request 生命周期,不能跨中间件持久化,所以必须由限流中间件主动 set - 若要返回重试时间(
Retry-After),得在 Lua 脚本里算好窗口结束时间戳,通过script.Run().Val()返回给中间件再写 header
为什么不用 iris-contrib/middleware/limiter?
这个包底层用的是内存计数器(sync.Map),不支持分布式部署。当你启多个 Iris 实例时,每个实例独立计数,实际限流效果归零。
- 它默认 key 是
ip + path,看似合理,但没解决多实例下数据不同步问题 - 没有提供 Redis 后端选项,也没暴露 Lua 脚本自定义能力,扩展性差
- 如果你项目已用 Redis,直接手写中间件反而更轻量、可控;如果还没引入 Redis,先评估是否真需要分布式限流——单机 QPS
真正容易被忽略的点:限流中间件的注册顺序。它必须在 mvc.Configure 之前,且在所有鉴权中间件之后(否则未登录用户也能耗尽配额)。另外,WebSocket 连接升级请求(Upgrade)不会经过普通中间件链,需单独为 /ws 路径配一套限流逻辑。











