revel 限流需自定义 filter,无内置组件;ip 限流须用 realip 解析并校验代理头;单机用 sync.map + rate.limiter,多实例必须 redis 原子 lua 脚本。

Revel 里限流必须自己写 Filter,没有内置限流中间件
Revel 框架本身不提供 RateLimiter 或类似 throttle 的开箱即用组件。所有限流逻辑都得靠自定义 Filter 实现,这点和 Gin、Echo 等轻量框架类似,但比 SpringBoot 的 @RateLimiter 或 Laravel 的 throttle:60,1 明显更底层。你不能在路由或 Controller 上加个注解就生效,必须手动插入过滤器链,并控制执行顺序。
常见错误是把限流逻辑写在 Before() 方法里——它只对当前 Controller 生效,且无法提前中断请求;正确位置是独立的 Filter,放在 RouterFilter 之后、ParamsFilter 之前,确保在参数解析前就拒绝非法请求。
- 限流 Filter 必须返回(不调用
fc(c, fcChain))才能终止请求流程 - 若需按 IP 或用户限流,
scope key要从c.Request中安全提取,不能直接用c.Request.RemoteAddr - Revel 的
Filter是同步阻塞式,不要在其中做耗时 Redis 查询或网络调用
IP 级限流要用 realip 解析,别信 X-Forwarded-For
Revel 应用通常部署在 Nginx 后面,c.Request.RemoteAddr 拿到的是 127.0.0.1:54321 这类本地地址,直接拿它做限流键等于完全失效。必须从请求头中提取真实客户端 IP,但又不能无条件信任 X-Forwarded-For —— 它可被伪造。
推荐做法是用 realip.FromHeaders()(如 github.com/zenazn/goji/web/util/realip)配合可信代理网段白名单:
ip := realip.FromHeaders(c.Request.Header, realip.Config{
Trusted: []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"},
})
同时确保 Nginx 配置中只由你可控的上游代理设置该头:
proxy_set_header X-Real-IP $remote_addr; # 不要用 $proxy_add_x_forwarded_for
- 如果 Revel 直连公网(不推荐),才 fallback 到
strings.Split(c.Request.RemoteAddr, ":")[0] - 提取出的 IP 建议做标准化(如 IPv4 统一、IPv6 压缩),避免
::1和127.0.0.1被当成两个 key - 测试时用
curl -H "X-Forwarded-For: 1.2.3.4" http://localhost:9000/api模拟,别只测本地回环
单实例限流用 sync.Map + rate.Limiter,别共享一个实例
Go 标准库的 golang.org/x/time/rate.Limiter 是最轻量、最可靠的选择,但它不是线程安全的全局对象。如果你在 Filter 里每次 new 一个 rate.NewLimiter(10, 5),会快速吃光内存;如果全局限流共用一个实例,所有请求就变成串行排队,彻底失去并发能力。
正确姿势是:按限流维度(如 IP)动态创建并缓存 *rate.Limiter,用 sync.Map 管理生命周期:
var limiters = sync.Map{} // key: ip string, value: *rate.Limiter
<p>func getLimiter(ip string) <em>rate.Limiter {
if l, ok := limiters.Load(ip); ok {
return l.(</em>rate.Limiter)
}
l := rate.NewLimiter(10, 5) // 10 QPS, burst=5
limiters.Store(ip, l)
return l
}
</p>
- burst 值建议设为期望 QPS 的 2–3 倍,应对短时突发
- 定期清理空闲限流器(比如 30 分钟未访问),避免 map 无限膨胀;可用
time.AfterFunc配合原子计数,但别用全局 timer -
limiter.Allow()返回 false 时,立刻c.Result = revel.JSON(c, map[string]string{"error": "too many requests"})并 return,不要继续走后续 filter
多实例部署必须上 Redis,INCR+EXPIRE 有竞态风险
单机 sync.Map 在 Revel 多实例场景下完全失效。此时必须引入 Redis 做分布式计数,但直接用 INCR + EXPIRE 两步操作存在竞态:INCR 成功后 EXPIRE 失败,key 就永久残留,导致后续所有请求都被误限。
唯一安全的做法是用 Lua 脚本一次性完成「读、增、设过期」三步原子操作:
local current = redis.call("INCR", KEYS[1])
if current == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[1])
end
return current
在 Revel Filter 中调用时传入 key(如 "rl:ip:" + ip)和窗口秒数(如 60),再判断返回值是否 ≤ 限流阈值。
- Redis key 命名要带业务前缀和维度标识,避免冲突;例如
"rl:ip:1.2.3.4"、"rl:user:1001" - 连接 Redis 推荐用连接池(如
github.com/go-redis/redis/v8),别每次请求都新建 client - 务必设置合理的超时(
Context.WithTimeout),防止 Redis 卡住整个请求链路
真正难的不是写通限流代码,而是决定“谁该被限”和“限多少”。同一个接口,登录用户、游客、爬虫 IP、健康检查探针,它们的合理配额可能差两个数量级;而 Redis 脚本里那个 ARGV[1](窗口秒数),一旦设成 60,就意味你放弃了滑动窗口精度——临界点前后两秒可能放行 2× 阈值的请求。这些权衡不会自动出现在你的 Filter 里,得靠日志、监控和线上反馈反复校准。











