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 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 不超阈值)。这种组合既能保证高吞吐,又能实现跨节点的一致性控制。