rate.Limiter 不能用于分布式限流,因其仅在单机内存维护状态,多实例各自计数导致限流失效;必须用 Redis + Lua 原子脚本实现,确保 INCR 与 EXPIRE 一次性执行。

rate.Limiter 不能用于分布式限流,它只在单机内存里维护状态,多实例下各自计数,等于没限。
为什么 rate.Limiter 在分布式场景下完全失效
常见错误是把 rate.NewLimiter 实例塞进 sync.Map 或全局变量,以为能“共享”。实际每个服务实例都有一套独立桶,压测时各节点都显示未超限,下游数据库却瞬间被打穿。
根本问题在于:没有原子性共享状态。哪怕加了锁,也无法跨进程同步。
- 同一用户在不同节点反复触发限流失败,或完全绕过
- 突发流量集中在某台机器,其他实例空闲却无法分担
-
rate.Limiter的Allow()依赖本地时间与内存变量,无法感知其他实例行为
必须用 Redis + Lua 实现原子计数
直接调 INCR + EXPIRE 是错的——两步非原子,服务崩溃或网络中断会导致 key 永不过期,后续所有请求被永久拒绝。
正确做法是把这两步封装进 Lua 脚本,在 Redis 服务端一次性执行:
立即学习“go语言免费学习笔记(深入)”;
local current = redis.call("INCR", KEYS[1])
if current == 1 then
redis.call("EXPIRE", KEYS[1], tonumber(ARGV[1]))
end
return current
关键实操点:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
-
script.Load()只需在启动时执行一次,别每次请求都重载 -
script.Eval()的keys参数必须是非空切片,比如[]string{key},否则报ERR wrong number of arguments -
ARGV[1]是窗口秒数(如60),传入时得转成字符串:strconv.Itoa(windowSec) - 返回值是
int64,直接和阈值比较:int(result)
Key 设计决定限流粒度,别写死
限流效果全靠 key 构造逻辑。写死 "rate:limit" 就是全站共用一个计数器;只用 userID 会忽略接口路径、IP、HTTP 方法等维度。
推荐按场景组合 key,例如:
- 按 IP 限流:
"rate:ip:" + clientIP - 按用户+接口限流:
"rate:user:" + userID + ":path:" + path - 按 IP + 方法 + 路径限流:
"rate:ip:" + ip + ":method:" + method + ":path:" + path
注意:key 中避免含特殊字符,建议用 url.PathEscape 或简单替换处理。
滑动窗口比固定窗口更抗突增
固定窗口(如“每分钟最多 100 次”)有临界问题:窗口切换瞬间可能放行双倍请求。滑动窗口统计“过去 60 秒内真实请求数”,精度更高。
实现上用 Redis ZSET 存时间戳,配合 Lua 原子执行:
-
ZREMRANGEBYSCORE清理过期时间戳 -
ZCARD获取当前窗口请求数 -
ZADD写入新请求时间戳,score 为 Unix 秒级时间戳 - 务必加
EXPIRE防 key 残留,但必须和 ZADD 同脚本原子执行
真正容易被忽略的是:滑动窗口的 Lua 脚本必须保证 ZADD 和 EXPIRE 在同一事务中,否则 ZSET 可能长期滞留无过期时间。

















