分布式限流

场景

负载均衡上线后,系统运行正常。

但过了一段时间,我发现了一个奇怪的问题:

某个免费用户的调用情况:

  • 套餐限额:1 次/秒
  • 实际调用:4 次/秒

为什么限流没有生效?

问题分析

我检查了限流逻辑,发现:

当前限流实现

设计流程
当前限流实现
  1. 步骤 1:读取用量并判断是否超过配额
  2. 步骤 2:在 Redis 中原子更新限流计数并设置窗口过期时间
  3. 步骤 3:校验身份、密钥或权限
关注点:限流维度、原子计数、误杀风险和告警阈值。

看起来没问题啊,Redis 是共享的。

深入调查

我查了一下日志,发现:

时间轴(User 123 的请求):

14:00:00.100 → Server 1: 允许(桶里有 10 个令牌)
14:00:00.200 → Server 2: 允许(桶里有 10 个令牌)
14:00:00.300 → Server 3: 允许(桶里有 10 个令牌)
14:00:00.400 → Server 4: 允许(桶里有 10 个令牌)

问题找到了!

每台服务器都认为桶里有 10 个令牌,所以都放行了。

根本原因

问题:竞态条件

设计流程
问题:竞态条件
  1. 步骤 1:更新「问题:竞态条件」依赖的数据、配置或运行状态
  2. 步骤 2:校验身份、密钥或权限
  3. 步骤 3:确认「问题:竞态条件」涉及的请求、数据对象和责任边界
  4. 步骤 4:根据输入、状态和失败情况选择后续路径
关注点:输入边界、状态变化、失败处理和验证方式。

结果:本该消耗 4 个令牌,实际只消耗了 1 个!

为什么之前没问题?

单机时代:
- 所有请求都在同一台服务器
- 单实例内部只有一份限流状态,冲突范围较小
- 不存在竞态条件

负载均衡后:
- 请求分配到不同的服务器
- 多台服务器同时读取和修改 Redis
- 存在竞态条件

解决方案

需要使用Redis 原子操作

选型边界
为什么不只靠普通 Redis 读写
触发问题
负载均衡后,多台服务器会同时读取和扣减同一个限流状态,普通 get/set 会产生竞态。
候选方案
Redis 事务、Lua 脚本、Redis-cell 模块、把限流状态收敛到单独服务。
选择理由
Lua 脚本能把读取、判断、扣减、设置过期时间放在 Redis 内一次原子执行,改造成本低。
代价
脚本需要版本管理和压测;复杂业务逻辑写进 Lua 后可读性不如应用代码。
暂不解决
暂不部署 Redis-cell 或独立限流服务,因为当前目标是先修复多实例下的超发问题。

方案 1:Redis 事务(WATCH + MULTI)

设计流程
方案 1:Redis 事务(WATCH + MULTI)
  1. 步骤 1:读取用量并判断是否超过配额
  2. 步骤 2:更新数据访问路径、归档标记或查询索引状态
  3. 步骤 3:校验身份、密钥或权限
关注点:一致性、查询性能、归档边界和可回滚性。

问题:

  • 性能较差(需要重试)
  • 复杂度高

方案 2:Lua 脚本(推荐)

设计流程
方案 2:Lua 脚本(推荐)
  1. 步骤 1:读取用量并判断是否超过配额
  2. 步骤 2:更新「方案 2:Lua 脚本(推荐)」依赖的数据、配置或运行状态
  3. 步骤 3:校验身份、密钥或权限
关注点:输入边界、状态变化、失败处理和验证方式。

优势:

  • 原子执行(Redis 保证)
  • 性能好(不需要重试)
  • 简单清晰

方案 3:Redis 模块(Redis-cell)

设计流程
方案 3:Redis 模块(Redis-cell):部署操作
  1. 步骤 1:准备运行环境并启动服务
  2. 步骤 2:读取调用方身份、限流维度和当前窗口计数
  3. 步骤 3:按配额、窗口和突发流量规则更新限流结果
  4. 步骤 4:返回 200、排队提示或 429,并记录限流原因
关注点:限流维度、原子计数、误杀风险和告警阈值。
设计流程
方案 3:Redis 模块(Redis-cell)
  1. 步骤 1:读取用量并判断是否超过配额
  2. 步骤 2:在 Redis 中原子更新限流计数并设置窗口过期时间
  3. 步骤 3:校验身份、密钥或权限
关注点:限流维度、原子计数、误杀风险和告警阈值。

优势:

  • 专门为限流设计
  • 功能强大
  • 性能最好

劣势:

  • 需要安装第三方模块
  • 不是所有 Redis 环境都支持

最终方案

我选择了Lua 脚本方案:

设计流程
最终方案
  1. 步骤 1:读取用量并判断是否超过配额
  2. 步骤 2:准备业务目标、系统边界、核心指标和取舍标准
  3. 步骤 3:用最终架构验证可用性、扩展性和成本取舍
  4. 步骤 4:用最终架构验证可用性、扩展性和成本取舍
关注点:设计取舍、职责边界、演进顺序和生产风险。