服务限流需用Redis+Lua实现全局控制,因Go标准库rate.Limiter仅单机有效;固定窗口适合容忍突刺场景,滑动窗口需ZSET+Lua保证原子性;工程上须关注连接池、超时、key命名与清理。

服务限流不是“挡流量”,而是用确定性规则保护后端不被压垮;单机rate.Limiter在多实例下完全无效,必须用 Redis + Lua 才算真正落地。
为什么 rate.Limiter 在微服务里基本不能直接用
Go 标准库的 rate.Limiter 是纯内存结构,每个进程独立维护令牌桶。5 个服务实例,每个都配了 “100 QPS”,实际就是 500 QPS 照单全收——这不是限流,是集体放行。
- 常见误用:每次 HTTP 请求都
new rate.Limiter(...),导致每个请求都有自己的桶 - 稍好一点但仍有缺陷:用
sync.Map按 IP 或用户 ID 缓存限流器,仍只在本机生效,跨节点无约束力 - 健康检查接口(如
/health)若被限流,K8s 会反复重启 Pod,形成雪崩
Redis 固定窗口限流:最简可用的 Lua 脚本
固定窗口适合登录、短信发送等对“突刺”容忍度较高的场景,实现简单、性能高,但要注意临界时间点可能翻倍放行。
下面这个脚本完成三件事:自增计数、首次写入时设过期、判断是否超限,全部原子执行:
立即学习“go语言免费学习笔记(深入)”;
local current = redis.call("INCR", KEYS[1])
if current == 1 then
redis.call("EXPIRE", KEYS[1], tonumber(ARGV[1]))
end
if current > tonumber(ARGV[2]) then
return 0
end
return current
-
KEYS[1]必须带业务前缀,例如"limit:login:ip:192.168.1.100",避免 key 冲突 -
ARGV[1]是窗口秒数(如"60"),ARGV[2]是阈值(如"10") - Golang 中调用要走
redis.NewScript()+script.Run(),别拼字符串,防注入 - 返回
0表示拒绝,>0 是当前计数值,可用于记录日志或打点
滑动窗口更准,但必须用 ZSET + Lua
如果要求“最近 60 秒内最多 100 次”,固定窗口会出问题(比如第 59 秒和第 61 秒各来 100 次)。这时得用 ZSET 存时间戳,靠 ZCOUNT + ZREMRANGEBYSCORE 维护滑动窗口。
关键不是“能不能做”,而是三步操作必须原子化:
- 先
ZREMRANGEBYSCORE清过期项 - 再
ZCOUNT查当前请求数 - 最后
ZADD记录新请求,并EXPIRE防 key 永久残留 - 这三步拆成多次 Redis 命令,在并发下必然计数错乱;只有 Lua 脚本能保证中间不被打断
别指望 pipeline 或客户端 sleep 补偿——网络延迟会让结果不可控。
容易被忽略的工程细节
限流逻辑本身不能成为瓶颈,否则保护没做成,先把自己拖垮。
- Redis 连接池
PoolSize至少设到50,尤其在网关层高并发场景 - 给 Redis 调用加超时,
Timeout: 100ms,超时建议直接放行(fail-open),别让限流变阻塞 - key 命名要细粒度:
"limit:svc:order-create:user:12345"比"order-create"更安全 - 别忘了清理闲置 key:Lua 脚本里
EXPIRE是基础,但异常中断可能导致 key 残留,可辅以定期 scan + del
真正难的不是写对一个 Lua 脚本,而是让所有服务实例共用同一套 key 策略、同一套超时配置、同一套降级行为——这些往往比算法本身更影响线上稳定性。


















