这是 Beta 探索课程,内容结构、实验步骤和示例可能会继续调整。
分布式限流
场景
负载均衡上线后,系统运行正常。
但过了一段时间,我发现了一个奇怪的问题:
某个免费用户的调用情况:
- 套餐限额:1 次/秒
- 实际调用:4 次/秒
为什么限流没有生效?
问题分析
我检查了限流逻辑,发现:
当前限流实现
设计流程
当前限流实现
- 步骤 1:读取用量并判断是否超过配额
- 步骤 2:在 Redis 中原子更新限流计数并设置窗口过期时间
- 步骤 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:更新「问题:竞态条件」依赖的数据、配置或运行状态
- 步骤 2:校验身份、密钥或权限
- 步骤 3:确认「问题:竞态条件」涉及的请求、数据对象和责任边界
- 步骤 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:读取用量并判断是否超过配额
- 步骤 2:更新数据访问路径、归档标记或查询索引状态
- 步骤 3:校验身份、密钥或权限
关注点:一致性、查询性能、归档边界和可回滚性。
问题:
- 性能较差(需要重试)
- 复杂度高
方案 2:Lua 脚本(推荐)
设计流程
方案 2:Lua 脚本(推荐)
- 步骤 1:读取用量并判断是否超过配额
- 步骤 2:更新「方案 2:Lua 脚本(推荐)」依赖的数据、配置或运行状态
- 步骤 3:校验身份、密钥或权限
关注点:输入边界、状态变化、失败处理和验证方式。
优势:
- 原子执行(Redis 保证)
- 性能好(不需要重试)
- 简单清晰
方案 3:Redis 模块(Redis-cell)
设计流程
方案 3:Redis 模块(Redis-cell):部署操作
- 步骤 1:准备运行环境并启动服务
- 步骤 2:读取调用方身份、限流维度和当前窗口计数
- 步骤 3:按配额、窗口和突发流量规则更新限流结果
- 步骤 4:返回 200、排队提示或 429,并记录限流原因
关注点:限流维度、原子计数、误杀风险和告警阈值。
设计流程
方案 3:Redis 模块(Redis-cell)
- 步骤 1:读取用量并判断是否超过配额
- 步骤 2:在 Redis 中原子更新限流计数并设置窗口过期时间
- 步骤 3:校验身份、密钥或权限
关注点:限流维度、原子计数、误杀风险和告警阈值。
优势:
- 专门为限流设计
- 功能强大
- 性能最好
劣势:
- 需要安装第三方模块
- 不是所有 Redis 环境都支持
最终方案
我选择了Lua 脚本方案:
设计流程
最终方案
- 步骤 1:读取用量并判断是否超过配额
- 步骤 2:准备业务目标、系统边界、核心指标和取舍标准
- 步骤 3:用最终架构验证可用性、扩展性和成本取舍
- 步骤 4:用最终架构验证可用性、扩展性和成本取舍
关注点:设计取舍、职责边界、演进顺序和生产风险。