直接用incr+expire组合会因网络抖动导致key永久残留或并发下多计数器并存,破坏限流一致性;必须用lua脚本保证原子性,否则高并发下必然超限或误拒。

不能只用 rdb.Incr + rdb.Expire 两步操作,否则高并发下必然超限或 key 永久残留。
为什么直接用 INCR 和 EXPIRE 组合会崩
网关是流量第一入口,每秒数千请求打进来时,INCR 返回 120,紧接着 EXPIRE 因网络抖动失败——这个 key 就再也不会过期,计数持续累加,后续所有请求都被误拒;更糟的是,多个请求同时发现 key 不存在,各自执行 SET + EXPIRE,导致同一窗口内出现多个独立计数器,限流阈值形同虚设。
- 根本问题不是 Redis 慢,而是 Go 层“先读再判再写”逻辑在分布式场景下天然不安全
-
rate.Limiter这类内存限流器在网关多实例部署下完全失效,各节点自说自话 - 哪怕用了
SET key value EX 60 NX,它也不支持原子自增,无法做令牌桶或滑动窗口
必须用 Lua 脚本封装完整限流逻辑
Redis 的 Lua 执行环境保证脚本内所有操作原子完成,这是唯一能守住一致性底线的方式。别图省事拼接字符串,直接用 redis.NewScript 加载并复用 EVALSHA。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 固定窗口限流脚本里,
redis.call("INCR", KEYS[1])后必须紧跟redis.call("EXPIRE", KEYS[1], ARGV[1]),且用if判断是否首次创建,避免重复设 TTL 导致覆盖 - 令牌桶脚本中,
HMGET读取last_ts和tokens必须一起,否则中间被其他请求修改就失准 - Go 接收返回值时,Lua 返回的数字默认是整型,要用
int64解包,否则redis.Int解析失败直接 panic - KEYS[1] 务必带业务前缀,比如
"gw:api:/order/create:192.168.3.15",防止不同接口或 IP 冲突
网关层调用时的关键参数控制
限流效果好不好,一半看脚本,一半看怎么传参。网关不像业务服务能等,必须快、稳、可退火。
-
ARGV中时间戳统一用毫秒级(time.Now().UnixMilli()),避免秒级精度在高频请求下把多个请求压进同一窗口 - TTL 设置不能拍脑袋:固定窗口建议设为窗口时长 × 1.2,令牌桶建议设为
capacity / rate * 1000毫秒(即桶清空所需最长时间),太短导致频繁重建 key,太长放大故障影响面 - 拒绝响应要明确:Lua 返回
{0, current_count}就立刻返回429 Too Many Requests,别重试——重试只会加剧竞争 - 连接池配置别吝啬:
MinIdleConns至少设为 10,MaxConnAge设为 30 分钟,避免 DNS 变更或主从切换后连接僵死
上线前必须验证的三个边界点
很多限流器上线后看似跑得通,一到大促就崩,往往栽在这三处。
- 脚本里没处理
redis.call("GET", key)返回false的情况,导致tonumber(false)得到nil,后续计算全错 - 网关转发时用了 client ip,但上游有 Nginx 或 ELB,实际拿到的是代理地址,必须从
X-Forwarded-For提取真实 IP 并清洗(防伪造) - 没设 fallback:当 Redis 不可用时,应降级为单机
rate.Limiter或直接放行,而不是卡住请求或全拒——后者比不限流还危险
真正难的不是写对一行 Lua,而是让整个链路在 Redis 故障、网络分区、客户端乱传 IP 等各种烂场景下,依然不雪崩、不误杀、不锁死。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










