rate.Limiter在多节点下失效,因其纯内存实现导致各节点独立计数;需改用Redis+Lua脚本实现原子化限流,避免竞态并确保全局一致性。

为什么直接用 rate.Limiter 在多节点下会失效
Go 标准库的 rate.Limiter 是纯内存实现,每个实例只在本机生效。部署多个服务节点时,各节点各自计数,总请求量轻松突破设定阈值——这不是“限流”,是“摆设”。真实场景中,只要流量打散到不同机器,rate.Limiter 就完全失去控制力。
必须把计数器和判断逻辑下沉到共享存储,而 Redis 是最常用、低延迟、支持原子操作的选择。关键不是“存数据”,而是用对 Redis 命令:不能靠 GET + INCR 两步走(竞态),得用 EVAL 或 INCRBY 配合过期时间一次性完成。
用 Lua 脚本保证原子性:一个可复用的令牌桶实现
Redis 自带 Lua 执行环境,能在一个原子操作里完成“读当前值 → 判断是否超限 → 更新计数 → 设置过期”全过程。下面这个脚本就是生产可用的最小闭环:
local key = KEYS[1]
local rate = tonumber(ARGV[1]) -- 每秒允许请求数
local capacity = tonumber(ARGV[2]) -- 桶容量(最大积压)
local now = tonumber(ARGV[3]) -- 当前时间戳(秒级或毫秒级需统一)
<p>local count = redis.call("GET", key)
if count == false then
count = 0
else
count = tonumber(count)
end</p><p>local allowed = 1
local new_count = count + 1</p><p>if new_count > capacity then
allowed = 0
end</p><p>-- 设置带过期的 key(注意:这里用的是秒级过期,对应 rate 单位)
redis.call("SET", key, new_count, "EX", 1)</p><p>return {allowed, new_count}立即学习“go语言免费学习笔记(深入)”;
调用时注意三点:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
KEYS[1]必须带业务前缀(如"rate:login:192.168.1.10"),避免键冲突 -
ARGV[3]推荐传毫秒时间戳(time.Now().UnixMilli()),但脚本里没用它做滑动窗口计算;如需精确漏桶/滑动窗口,得改脚本加ZSET或LIST结构 - 返回值是数组:
{1, 12}表示放行且当前计数为 12;{0, 12}表示拒绝
Golang 调用 Redis Lua 的实际写法(github.com/go-redis/redis/v8)
别用 Eval 硬拼字符串,先加载脚本再 EvalSha,既安全又高效:
script := redis.NewScript(`
local key = KEYS[1]
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local count = redis.call("GET", key) or 0
if tonumber(count) + 1 > capacity then
return {0, tonumber(count)}
end
redis.call("INCR", key)
redis.call("EXPIRE", key, 1)
return {1, tonumber(count) + 1}
`)
<p>// 调用
result, err := script.Eval(ctx, rdb, []string{key}, rate, capacity).Result()
if err != nil {
// 注意:Redis 返回 nil 不代表失败,要检查 err
return false, err
}</p><p>res := result.([]interface{})
allowed := res[0].(int64) == 1常见坑:
- 没设
context.WithTimeout,Redis 挂了导致整个 HTTP 请求卡死 - 忘记给
key加 namespace,比如所有接口共用"rate",互相干扰 - 误用
INCR而不设过期——key 永久存在,最终爆满
要不要用 Redlock 或分布式锁?大多数情况不用
限流本质是“允许/拒绝”,不是“修改共享状态”,不需要强一致性。上面 Lua 脚本已通过 Redis 单线程特性保证原子性,INCR + EXPIRE 组合在绝大多数场景下足够可靠。
Redlock 增加复杂度、降低吞吐,还引入时钟漂移等新问题。除非你的限流策略依赖跨 key 协调(例如:用户 A 在 API1 和 API2 的总调用量受限),否则别碰分布式锁。
真正该花精力的地方是降级:当 Redis 不可用时,是直接放过所有请求(fail-open),还是 fallback 到本地 rate.Limiter(fail-close)?这取决于业务容忍度——登录接口通常选 fail-close,搜索接口可能选 fail-open。

















