rate.Limiter无法用于分布式限流,因其仅在单机内存维护令牌桶状态,多实例下各自独立计数导致总流量失控;必须依赖Redis+Lua实现滑动窗口等原子性限流方案。

单机 rate.Limiter 无法跨实例共享状态,分布式限流必须引入外部存储,Redis 是最常用且可靠的选择。直接用 go-redis + Lua 脚本实现滑动窗口,比简单计数器更能应对临界突增流量。
为什么不能只用 rate.NewLimiter 做分布式限流
rate.Limiter 是纯内存、单 goroutine 安全的结构,不支持跨进程/跨机器同步。即使你把同一个 Limiter 实例塞进 sync.Map 或全局变量,多个服务实例仍各自维护独立桶状态,实际是“伪分布式”。常见错误现象包括:
- 压测时各实例都显示未超限,但下游数据库已被打满
- 相同用户在不同节点反复触发限流失败,或完全绕过限流
- 突发流量集中在某一台机器上,其他实例空闲却无法分担
根本原因:没有原子性共享状态。必须依赖 Redis 这类支持 CAS 或 Lua 原子执行的存储。
用 Redis + Lua 实现滑动窗口令牌桶(推荐)
比起固定窗口(如“每分钟最多 100 次”),滑动窗口更精准——它统计“过去 60 秒内”的真实请求数,避免窗口切换时的双倍放行问题。关键点:
立即学习“go语言免费学习笔记(深入)”;
- 用
ZSET存储每个请求的时间戳,score 为 Unix 时间戳(秒级或毫秒级) - 每次请求前,先用
ZREMRANGEBYSCORE清理过期时间戳 - 再用
ZCARD获取当前窗口内请求数,若 ≥ 阈值则拒绝 - 所有操作封装在 Lua 脚本中执行,保证原子性
示例 Lua 脚本(保存为 sliding_window.lua):
local key = KEYS[1]
local window = tonumber(ARGV[1]) -- 窗口秒数,如 60
local limit = tonumber(ARGV[2]) -- 最大请求数,如 100
local now = tonumber(ARGV[3]) -- 当前时间戳(秒)
<p>-- 清理过期元素
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)</p><p>-- 获取当前窗口请求数
local count = redis.call('ZCARD', key)</p><p>if count < limit then
redis.call('ZADD', key, now, now .. ':' .. math.random(1000, 9999))
redis.call('EXPIRE', key, window + 1)
return 1
else
return 0
endGo 中调用方式(使用 github.com/go-redis/redis/v9):
script := redis.NewScript(slidingWindowLua)
result, err := script.Run(ctx, rdb, []string{key}, windowSec, maxReq, time.Now().Unix()).Result()
if err != nil {
// 处理 Redis 错误
}
if result == int64(0) {
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
return
}如何生成限流 key:按用户/IP/接口组合
硬编码一个全局 key 就退化成全局限流,失去业务意义。真实场景需动态拼接标识,例如:
- 按用户限流:
"user:" + userID + ":api:/order/create" - 按 IP + 接口限流:
"ip:" + realIP + ":path:" + r.URL.Path - 按租户限流:
"tenant:" + tenantID + ":qps:50"
注意:userID 必须来自可信头(如内部鉴权后注入的 X-User-ID),不能直接取 Authorization 或参数,否则可被伪造。同时要对 key 做长度限制和非法字符过滤,防止 Redis key 溢出或注入。
容易被忽略的运维细节
上线后真正卡住人的往往不是算法,而是这些点:
- Redis 连接池没配
MinIdleConns,冷启动时大量timeout错误 - 没给 ZSET key 设置
EXPIRE,长期累积导致内存暴涨(脚本里已写,但易被删掉) - 时钟不同步:多台应用服务器时间差 > 窗口大小,会导致窗口计算错乱;建议统一 NTP 校时
- 没监控
redis_rate_limited_total和redis_rate_allowed_total指标,无法定位是策略过严还是攻击行为
滑动窗口的精度取决于时间戳粒度(秒 or 毫秒)和 Lua 执行延迟,高 QPS 场景下建议用毫秒级并压测验证 P99 延迟是否可控。不要为了“理论精确”牺牲可用性。


















