分布式限流实现方法详解(Redis + Lua 原子计数原理)

分布式限流的本质一句话:让集群里所有网关节点共享同一个计数器,并用原子操作完成”读取—判断—写入”,避免并发超发。落地标配是 Redis 存状态、Lua 脚本保证原子性。企业级 AI 网关接入多家大模型后,限流不再只是挡 QPS,还要按用户、API Key、模型维度分别计数,单机计数器分摊到多节点后必然失真,所以方案从一开始就要按分布式设计。

一、单机限流为什么在集群下失效

单机方案用 JVM 内存计数器,Guava 的 RateLimiter 就是典型实现。部署单实例时它能精确卡住每秒请求数,但网关一扩容到三个节点,流量被负载均衡打散,每个节点各数各的,三个节点加起来是阈值的三倍。要恢复准确,只能把计数状态挪到所有节点都能访问的地方,Redis 因此成为首选:状态集中、访问低延迟、原生支持原子命令。

二、四种限流算法怎么选

算法 允许突发 精度 实现复杂度 适用场景
固定窗口 否 低,有边界突刺 低 简单总量控制
滑动窗口 部分 高 中 秒杀、接口防刷
漏桶 否 中 中 流量整形、保护下游
令牌桶 是 中 中 API 网关、服务调用

AI 网关保护的是模型后端,模型接口对瞬时突发的容忍度取决于厂商侧配额,一般选令牌桶:既卡平均速率,又允许攒下的令牌支撑突发。滑动窗口精度更高,适合需要严格卡尖峰的计费接口。

三、Redis + Lua 的实现原理

3.1 多命令串行会超发

用 RedisTemplate 写”先查计数再自增”的 Java 代码,两个并发请求可能同时读到剩余额度,然后各自放行,超发就发生了。正确做法是把检查与扣减打包成一段 Lua 脚本,在 Redis 服务端单线程执行,天然串行化,命令之间不会被其他请求插入。

3.2 令牌桶的 Lua 脚本

-- 令牌桶:Redis + Lua 原子限流
local key = KEYS[1]
local rate = tonumber(ARGV[1])  -- 每秒补充令牌数
local cap = tonumber(ARGV[2])   -- 桶容量,即突发上限
local now = tonumber(ARGV[3])   -- 毫秒时间戳
local data = redis.call("HMGET", key, "tokens", "ts")
local tokens = tonumber(data[1]) or cap
local ts = tonumber(data[2]) or now
-- 按流逝时间补充令牌,不超过桶容量
tokens = math.min(cap, tokens + (now - ts) / 1000 * rate)
local allowed = 0
if tokens >= 1 then
    tokens = tokens - 1
    allowed = 1
end
redis.call("HMSET", key, "tokens", tokens, "ts", now)
redis.call("PEXPIRE", key, math.ceil(cap / rate * 1000))
return {allowed, math.floor(tokens)}

脚本返回是否放行与剩余令牌数,网关拿到 allowed 为 0 时返回 HTTP 429,并带 Retry-After 头引导客户端退避。键的过期时间由容量与速率推算,空闲限流键自动回收,不会在 Redis 里堆垃圾。

3.3 脚本执行的性能细节

每次调用都整段传输脚本会有网络开销,生产上改用 EVALSHA:先上传脚本拿到 SHA1,后续只传哈希引用,Redis 直接执行缓存的脚本。脚本未缓存时兜底回退 EVAL 上传一次。这套组合把单次限流判定的网络往返压到一次,网关高并发下不会让 Redis 成为瓶颈。选型上固定窗口代码最少,适合粗粒度总量控制;滑动窗口与令牌桶是生产主力,按维度精度要求取舍即可。

3.4 滑动窗口用有序集合实现

精度要求高的维度改用滑动窗口:把请求时间戳当作分数写入有序集合,每次请求先清理窗口外的旧记录,再统计当前窗口内数量,超阈值即拒绝。

-- 滑动窗口:清理 + 计数 + 写入,原子完成
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])   -- 窗口毫秒
local limit = tonumber(ARGV[3])
redis.call("ZREMRANGEBYSCORE", key, 0, now - window)
local count = redis.call("ZCARD", key)
if count >= limit then return 0 end
redis.call("ZADD", key, now, ARGV[4])
redis.call("PEXPIRE", key, window / 1000)
return 1

每笔请求在集合里占一条记录,几十字节的开销换来了无边界突刺的精确控制,适合计费与防刷这类对尖峰敏感的维度。

四、AI 网关限流的落地分层

  1. 接入层按 IP 与总 QPS 限流,挡住无差别打流量;
  2. 业务层按 API Key 限流,每个客户账户独立配额,互不影响;
  3. 模型层按 model 维度限流,热门模型单独保护,防止一个模型挤占全部额度;
  4. 消耗层按 token 估算限流,流式会话长,用并发会话数与输入输出量双重约束。

落地时还有两个兜底:Redis 宕机不能连带网关不可用,客户端本地判断降级为放行并告警,宁可短时超限也不阻断全部请求;服务重启后令牌桶从空桶起步,突发流量会瞬间打满,加预热模式逐步放开阈值。

对话网关的 SSE 长连接要单独处理。流式连接一旦建立会占住通道数分钟,限流判定应落在”连接建立”这一时刻,而不是生成中途。同时把并发连接数也纳入限流维度,配合令牌桶按连接数与输入输出速率双重约束,防止少数长连接拖垮线程池。

五、现成方案与自研的取舍

现成的分布式限流有 Redisson 的 RRateLimiter、Spring Cloud Gateway 的 RequestRateLimiter 过滤器、阿里的 Sentinel。它们的差别在于控制粒度与集成深度:Redisson 提供现成令牌桶实现,接入快;Sentinel 侧重服务内部资源级防护与动态规则,限流数据可以同步到控制台观察;自研 Lua 的优势是多维限流键可定制、不引入额外组件依赖,代价是要自己维护脚本与告警。

从监控角度想清楚一件事:限流命中率本身是核心指标。每次判定都把维度键、放行与否、剩余额度写入日志或指标,线上才能回答”哪个客户被限了、卡在哪个模型、阈值合不合理”这类问题。没有观测的限流形同虚设,调参全靠猜。

常见问题(FAQ)

Q1:为什么必须用 Lua 脚本而不是多个 Redis 命令?

多命令之间存在间隙,并发时读到旧值导致超发。Lua 在服务端原子执行。

Q2:限流超了该返回什么?

返回 429 Too Many Requests,附带 Retry-After 与限流相关响应头。

Q3:Redis 挂了网关怎么办?

本地降级放行并告警,保障主链路可用,恢复后自动回到精确限流。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
赵其鑫的头像赵其鑫管理团队

相关推荐

返回顶部