4. 分布式限流挑战:集群环境下计数器同步问题,Redis+Lua 实现原子性限流

在云原生网关的集群部署模式下,单机限流(如 Guava RateLimiter、AtomicLong 本地计数器)会因节点间无共享状态而失效。假设网关有 3 个 Pod,每个 Pod 本地允许 100 QPS,集群总 QPS 可能达到 300,完全突破限流阈值。分布式限流的核心挑战在于:如何在多节点间维护一个精确、一致、高性能的全局计数器。

4.1 集群计数器同步的三大挑战

挑战 描述 典型后果
竞态条件 多个网关节点同时读取并写入同一计数器,导致计数丢失或超发 限流阈值被突破,后端服务过载
时钟偏差 各节点系统时间不一致,滑动窗口或令牌桶的“时间窗口”无法对齐 限流窗口错位,部分节点提前重置计数器
网络延迟与分区 计数器同步依赖远程调用(如 Redis),网络抖动导致读写超时或数据不一致 限流决策延迟,或降级为“熔断放行”
关键结论: 分布式限流必须依赖一个外部原子化存储(如 Redis)来协调所有网关节点的计数器状态,且操作必须满足原子性,避免“读-改-写”三步之间的竞态窗口。

4.2 为什么选择 Redis + Lua?

Redis 的单线程模型天然支持原子操作,但单纯使用 INCR + EXPIRE 组合仍存在竞态:两个节点先后执行 GET 判断阈值,然后 INCR,中间可能被其他节点插入。Lua 脚本将多个 Redis 命令打包为一个原子执行单元,脚本执行期间不会被其他命令中断,从而彻底消除竞态条件。

方案 原子性 网络往返 适用场景
Redis 单命令(INCR+EXPIRE) ❌ 非原子 2 次 低并发、允许少量超发
Redis 事务(MULTI/EXEC) ✅ 原子,但不可编程 2 次 简单批量操作
Redis + Lua 脚本 ✅ 原子 + 可编程 1 次 生产级分布式限流

4.3 基于 Redis + Lua 的滑动窗口限流实现

以下是一个生产级滑动窗口限流 Lua 脚本,支持毫秒级精度,通过有序集合(Sorted Set)存储每个请求的时间戳,窗口内请求数超过阈值则拒绝。

-- KEYS[1]: 限流 key,例如 "rate_limit:api:/v1/order"
-- ARGV[1]: 窗口大小(毫秒),例如 1000
-- ARGV[2]: 窗口内最大请求数,例如 100
-- ARGV[3]: 当前时间戳(毫秒),由网关传入避免时钟偏差

-- 1. 移除窗口外的过期元素
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[3] - ARGV[1])

-- 2. 统计当前窗口内请求数
local current = redis.call('ZCARD', KEYS[1])

-- 3. 判断是否超过阈值
if current < tonumber(ARGV[2]) then
    -- 未超限:添加当前请求时间戳
    redis.call('ZADD', KEYS[1], ARGV[3], ARGV[3] .. ':' .. math.random())
    -- 设置 key 过期时间,避免内存泄漏
    redis.call('PEXPIRE', KEYS[1], ARGV[1])
    return 1  -- 放行
else
    return 0  -- 拒绝
end
最佳实践: 网关节点在调用 Lua 脚本前,统一从 Redis 获取当前时间(TIME 命令),或由网关自身生成单调递增的时间戳,避免各节点时钟偏差导致窗口错位。脚本内使用 math.random() 确保同一毫秒内多个请求的 member 唯一。

4.4 原子性限流架构流程

下图展示了三个网关节点通过 Redis + Lua 脚本实现原子性限流的完整交互流程:

Redis + Lua 原子性限流架构流程 网关节点 1 请求到达 网关节点 2 请求到达 网关节点 3 请求到达 Redis 集群 执行 Lua 脚本(原子操作) 返回 1(放行) 返回 0(拒绝) 返回 1(放行)
流程说明: 所有网关节点共享同一个 Redis key,Lua 脚本在 Redis 内部完成“清理过期元素 → 统计当前计数 → 判断阈值 → 写入新元素”的完整逻辑,整个过程对网关节点透明且原子。返回 1 表示放行,返回 0 表示拒绝,网关根据返回值执行对应的限流动作(如返回 429 或降级)。

4.5 生产环境注意事项

  • Redis 高可用: 使用 Redis Sentinel 或 Redis Cluster 避免单点故障;限流 key 应使用 hash tag(如 {rate_limit}:api:v1)确保同一限流 key 落在同一分片。
  • 脚本缓存: 使用 SCRIPT LOAD 预加载 Lua 脚本,后续通过 EVALSHA 调用,减少网络传输开销。
  • 降级策略: 当 Redis 不可用时,网关应降级为本地限流(允许一定超发),而非直接熔断所有请求。
  • 内存控制: 滑动窗口使用有序集合存储每个请求的时间戳,高 QPS 场景下需设置合理的窗口大小和过期时间,避免 Redis 内存暴涨。
常见陷阱: 不要在 Lua 脚本中使用 KEYS 以外的动态 key 名(如拼接字符串),这会导致 Redis Cluster 模式下跨分片错误。所有限流 key 必须通过 KEYS 数组传入。

4.6 性能对比:本地限流 vs 分布式限流

指标 本地限流(AtomicLong) 分布式限流(Redis+Lua)
延迟(P99) < 1ms 1-5ms(取决于网络)
集群一致性 ❌ 无 ✅ 强一致
吞吐量(单节点) 10万+ QPS 1-5万 QPS(受 Redis 限制)
适用场景 单机限流、降级兜底 集群精确限流、跨节点配额

在实际云原生网关中,通常采用两级限流架构:第一级使用本地限流做快速过滤(拦截大部分突发流量),第二级使用 Redis+Lua 做全局精确限流(确保集群总 QPS 不超阈值)。这种组合既能保证高吞吐,又能实现跨节点的一致性控制。