rate.Limiter 无法用于分布式限流,因其状态仅存于本地内存,多实例间无同步机制,导致总并发达限制值的N倍;必须用 Redis+Lua 原子脚本实现全局一致令牌桶。
go 标准库的 golang.org/x/time/rate 提供了单机版令牌桶,但直接用于分布式场景会失效——多个实例各自维护独立桶,无法协同计数。必须借助外部共享存储(如 redis)+ 原子操作来实现一致性限流。
为什么不能直接用 rate.Limiter 做分布式限流
它所有状态(当前令牌数、上次填充时间)都存在内存里,无跨进程同步机制。哪怕你用相同参数初始化 10 个服务实例,每个都从满桶开始,实际总并发可能飙到 10× 限制值。
- 错误现象:
rate.Limiter.Allow()在压测中始终返回true,QPS 远超预期 - 根本原因:没有全局共享的“桶”,只有本地幻觉
- 适用场景:仅限单进程内限流(如 HTTP handler 内部防误调用)
用 Redis + Lua 实现原子令牌桶操作
Redis 的 EVAL 支持 Lua 脚本原子执行,能在一个请求里完成「读桶状态 → 计算新令牌数 → 判断是否允许 → 写回」全过程,避免竞态。Go 端只需调用一次 redis.Client.Eval()。
- 关键点:Lua 脚本必须包含完整令牌计算逻辑(含时间戳更新、令牌累加、上限截断)
- 推荐脚本结构:输入 key、capacity、fillRate、now;输出 1(允许)或 0(拒绝)+ 当前剩余令牌
- 注意 Redis 时间精度:用
redis.call("TIME")获取秒+微秒,比客户端传时间更可靠 - 示例片段(Lua):
local key = KEYS[1] local capacity = tonumber(ARGV[1]) local fillRate = tonumber(ARGV[2]) local now = tonumber(ARGV[3]) local bucket = redis.call("HMGET", key, "tokens", "last_fill") local tokens = tonumber(bucket[1]) or capacity local last_fill = tonumber(bucket[2]) or now local delta = math.max(0, now - last_fill) local new_tokens = math.min(capacity, tokens + delta * fillRate) if new_tokens >= 1 then redis.call("HMSET", key, "tokens", new_tokens - 1, "last_fill", now) return 1 else redis.call("HMSET", key, "tokens", new_tokens, "last_fill", now) return 0 end
Go 客户端封装要点:重试、降级与误差容忍
网络抖动或 Redis 故障时,不能让限流器直接 panic 或阻塞请求。需定义明确的 fallback 行为。
- 连接失败时,默认放行(
Allow() == true),避免雪崩;可通过配置开关改为拒绝 - 对
redis.Eval()设置超时(建议 ≤ 50ms),超时即走降级路径 - 避免高频打 Redis:对同一 key 的连续请求可缓存“桶状态”最多 100ms(需记录 last_check 时间戳)
- 填速率单位统一用「令牌/秒」,但 Lua 中需转为「令牌/毫秒」参与计算,否则浮点精度丢失严重
- 示例调用:
func (l *RedisLimiter) Allow(ctx context.Context, key string) (bool, error) { now := time.Now().UnixMilli() res, err := l.client.Eval(ctx, luaScript, []string{key}, l.capacity, float64(l.fillRate)/1000.0, now).Result() if err != nil { if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, redis.Nil) { return l.fallback, nil // 降级策略 } return false, err } return res.(int64) == 1, nil }
真正难的不是写对 Lua 脚本,而是处理时钟漂移、Redis 集群分片导致的 key 分布不均、以及高并发下 EVAL 的性能毛刺。建议上线前用 go tool pprof 抓一次 CPU profile,确认 redis.Client.Eval 不是瓶颈热点。


















