因为分布式环境下各节点独立维护令牌桶,无法协同计数,ratelimiter会失效;真正可用的方案必须共享状态,如redis+lua实现原子性滑动窗口限流。

限流为什么不能只靠单机 rate.Limiter
因为分布式环境下,每个服务实例都独立维护自己的令牌桶,请求打到不同节点时无法协同计数,rate.Limiter 会失效。比如你设了每秒 100 次请求,但 4 个实例各自放行 100 次,实际峰值就是 400 次——这不是限流,是放大流量。
真正可用的方案必须共享状态。常见选择有 Redis(带原子操作)、etcd(强一致性)、或专用限流服务(如 Sentinel)。Gin 本身不提供分布式限流中间件,得自己封装。
用 Redis + Lua 实现原子性滑动窗口限流
滑动窗口比固定窗口更平滑,避免“窗口切换瞬间突增”问题;而用 Lua 脚本在 Redis 里执行,能保证 GET、INCR、EXPIRE 三步不被并发打断。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- Key 设计:用
rate:ip:192.168.1.100或rate:uid:12345区分维度,避免全局锁 - Lua 脚本要返回两个值:当前计数、是否允许通过(1/0)
- Gin 中间件里用
redis.Client.Eval()调用,别用多个独立命令拼接 - 注意 Redis 连接池配置——高并发下连接耗尽比限流失败更致命
local key = KEYS[1]
local window = tonumber(ARGV[1])
local max = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local window_start = now - window
<p>local entries = redis.call('ZRANGEBYSCORE', key, 0, window_start)
if #entries > 0 then
redis.call('ZREM', key, unpack(entries))
end</p><p>local count = redis.call('ZCARD', key)
if count >= max then
return {count, 0}
end</p><p>redis.call('ZADD', key, now, now)
redis.call('EXPIRE', key, window + 1)
return {count + 1, 1}</p>
Gin 中间件怎么注入限流逻辑而不拖慢请求
限流检查必须在路由匹配前完成,否则可能绕过;但阻塞式 Redis 调用会让整个 HTTP 处理卡住。关键点不在“加不加中间件”,而在“怎么加才不伤性能”。
- 用
ctx.Request.Context()传入超时控制,比如设置redis.Context.WithTimeout(ctx, 100 * time.Millisecond) - 失败策略选“放行”而非“拒绝”——限流组件挂了,业务不能跟着雪崩
- 对高频路径(如健康检查
/health)显式跳过限流,用strings.HasPrefix(ctx.Request.URL.Path, "/health") - 记录被拒请求时,只打结构化日志(含 key、窗口、当前 count),别打完整 body
测试时发现“同一 IP 总是被限”?检查这几个地方
真实部署后常出现误限,往往不是算法问题,而是基础设施细节没对齐:
- 反向代理(Nginx / ALB)没透传真实 IP,
c.ClientIP()拿到的是代理地址——改用c.GetHeader("X-Real-IP")或配置TrustedProxies - Redis 时间和应用服务器时间偏差超过窗口长度(比如窗口 60s,时钟差 65s),导致 ZRANGEBYSCORE 删不掉旧数据——统一 NTP 同步
- 滑动窗口脚本里
EXPIRE的 TTL 设太短,比如窗口 60s 却只设EXPIRE key 30,中间件反复重建 key 导致计数丢失 - Gin 的
Use()顺序写错,把限流中间件放在了Recovery()后面,panic 时根本走不到限流逻辑
分布式限流真正的复杂点不在代码多难写,而在于边界条件太多:时钟、网络、代理、降级策略,全得串起来验证。少测一个环节,上线就成“间歇性抽风”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










