必须用lua脚本实现令牌桶,因其在redis单线程中执行可保证“读-判-写”原子性,避免客户端多请求并发导致的竞态超卖;所有操作须封装于单次eval/evalsha调用中。

直接用 INCR + EXPIRE 两步操作做防刷,一定会漏判、超限放行——Redis 命令不是原子的,高并发下两个请求同时读到 0,都执行 EXPIRE,key 过期时间被重置,计数却翻倍。
为什么 Redis+Lua 是分布式防刷的底线
防刷本质是「单位时间窗口内计数 + 判断 + 设置过期」三件事必须串行执行。Lua 脚本在 Redis 单线程中运行,天然原子。别信 pipeline 或事务:它们只保证命令发送顺序,不保证中间没其他客户端插入操作。
-
redis.Eval调用前必须预编译脚本对象(redis.NewScript),避免每次解析开销 - 脚本里第一行必须做
EXISTS判断,否则新 key 第一次INCR返回 0,误判为“未超限” - 传参要用
KEYS[1](key 名)和ARGV[1](阈值)、ARGV[2](窗口秒数),别硬编码 - 返回值类型要检查:
int64> 0 表示放行(当前计数),0 表示拒绝,redis.Nil是正常响应,网络错误才需告警
Gin 中间件里怎么安全提取限流标识
IP 和用户 ID 必须从可信来源取,不能依赖 c.ClientIP() 或裸 GET 参数。Nginx 转发时若没配 proxy_set_header X-Real-IP $remote_addr,c.ClientIP() 就是 127.0.0.1。
- 真实 IP 应优先取
c.GetHeader("X-Real-IP"), fallback 到c.GetHeader("X-Forwarded-For")的第一个非内网地址 - 用户 ID 必须来自认证中间件之后的上下文(如
c.GetString("user_id")),绝不能在防刷中间件里查 DB - key 拼接格式建议:
"rate:ip:" + ip + ":path:" + c.Request.URL.Path或"rate:uid:" + uid + ":api:" + c.Request.URL.Path - 登录接口和注册接口必须分 key,否则注册刷崩会导致登录不可用
令牌桶 vs Redis Lua:什么时候该切方案
单机部署且 QPS golang.org/x/time/rate.Limiter 更轻量;但只要上 Kubernetes 或多 Pod,就必须切 Redis+Lua,否则每个实例各自为政,总量完全失控。
-
rate.NewLimiter(rate.Every(time.Second/10), 5)表示 10 QPS,突发最多 5 个请求 ——Burst不是缓冲区大小,是允许的瞬时峰值 - 用
sync.Map缓存*rate.Limiter实例时,key 是字符串(如 IP),但必须配后台 goroutine 定期清理 5 分钟未访问的 entry,否则内存泄漏 - Redis 方案里,窗口秒数(如 60)和阈值(如 100)应配置化,别写死;不同接口按风险等级设不同组合
- 别把限流逻辑塞进业务 handler 里 —— 必须在 Gin 中间件最外层拦截,否则请求已进业务层,数据库或下游服务可能已被打穿
真正难的不是写脚本或调 API,而是维度设计是否贴合业务场景:同一个用户换设备、同一个 IP 多用户、代理穿透、JWT 过期后重放……这些边界 case 没覆盖,防刷就形同虚设。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











