方案三:Redis + Lua 分布式限流
1. 业务场景
在报表查询业务中,多个用户可能同时发起查询请求。如果请求集中到达,业务服务会产生大量数据库查询,导致数据库 CPU 升高、连接池被占满,进而影响其他正常业务。
如果只在单个应用实例中使用本地计数器,各实例的限流状态相互独立,无法准确控制整个集群的请求总量。
因此,可以使用 Redis 统一维护限流状态,通过 Lua 脚本原子执行限流判断,使多个应用实例共享同一套限流额度。
2. 整体方案设计
请求经过 Spring Cloud Gateway 时,网关先识别用户身份和目标接口,再调用 Redis 执行 Lua 脚本,判断当前请求是否允许通过。
整体链路如下:
客户端请求 → Spring Cloud Gateway → 获取用户 ID 和接口标识 → Redis 执行 Lua 限流脚本 → 根据结果放行或拒绝 → 业务服务 → 数据库。
其中:
- Spring Cloud Gateway:负责识别请求、执行限流过滤器,以及放行或拒绝请求。
- Redis:集中保存限流状态,供多个网关实例共享。
- Lua 脚本:在 Redis 内完成额度检查和状态更新,避免并发请求之间出现计数竞争。
- 业务服务:处理通过网关检查的请求,并访问数据库。
将限流放在网关入口,可以尽量避免超额请求进入业务服务,减少无效数据库查询。
3. 工程实施步骤
第一步:梳理业务接口和限流目标
首先分析数据库压力主要来自哪些接口,例如报表查询、数据导出和复杂统计接口。
结合接口的数据库开销、正常请求量和业务优先级,确定限流规则。
例如:
| 限流维度 | 示例规则 | 目的 |
|---|---|---|
| 用户级 | 每个用户每秒最多请求 5 次 | 防止单个用户持续占用资源 |
| 接口级 | 报表查询接口每秒最多放行 500 次 | 控制接口整体流量 |
| 并发级 | 同时执行的重型报表查询不超过 30 个 | 避免慢查询长期占用数据库连接 |
以上数值仅用于说明设计方式,实际阈值需要根据压测结果和数据库承载能力确定。
需要注意:请求速率限制与并发限制是两种不同的控制手段。每秒 500 次的请求限制,并不意味着同时执行的数据库查询只有 500 个或更少。
第二步:设计 Redis 限流 Key
根据限流维度生成不同的 Key,例如:
rate:user:1001:reportrate:global:report第一个 Key 记录用户 1001 对报表接口的限流状态,第二个 Key 记录报表接口的全局限流状态。
实际项目中,还需要考虑:
- 为不同接口配置独立的限流规则。
- 在 Key 中加入必要的业务标识,避免不同业务共享错误的计数。
- 对限流状态设置合理的过期时间,避免无用数据长期堆积。
- 如果采用滑动窗口或令牌桶,需要根据算法设计对应的数据结构和 Key。
第三步:编写 Lua 脚本,实现原子限流
根据业务要求选择限流算法。例如,使用固定窗口时,可以通过 Redis 计数器记录指定时间窗口内的请求数量。
Lua 脚本在一次执行中完成计数更新、额度判断和结果返回,避免多个请求先后读取同一个旧计数、同时判断通过的问题。
执行逻辑可以概括为:
- 读取当前窗口的请求计数。
- 判断计数是否达到限额。
- 如果已经达到限额,返回拒绝结果。
- 如果尚未达到限额,更新计数并设置过期时间,然后返回放行结果。
如果同时实施用户级和接口全局级限流,应当在同一次脚本执行中检查相关额度,并确保只有所有必要的限流规则均通过时才扣减额度,避免其中一项失败却消耗另一项额度。
固定窗口实现简单,但在窗口交界处可能出现突发流量。如果业务对短时流量更加敏感,可以改用滑动窗口或令牌桶算法。
第四步:在 Gateway 中接入限流逻辑
在 Spring Cloud Gateway 中,可以通过自定义 GlobalFilter 或 GatewayFilter 接入限流逻辑。
过滤器执行以下操作:
- 从经过认证的请求上下文中获取用户 ID。
- 根据请求路径或路由信息识别目标接口。
- 根据用户和接口生成限流参数。
- 调用 Redis 执行 Lua 脚本。
- 根据脚本结果决定是否继续执行过滤器链。
如果限流通过,则调用过滤器链继续转发请求;如果超过额度,则直接返回 HTTP 429 Too Many Requests,不再访问下游业务服务。
这里要注意,用户 ID 应当来自经过验证的身份信息,而不是直接信任客户端任意传入的请求头,否则用户可以伪造身份绕过用户级限流。
第五步:处理异常和降级
Redis 限流属于请求入口的关键依赖,需要提前设计异常处理策略。
- 超过限额:返回 HTTP 429,并提供适当的错误提示。
- Redis 超时或不可用:根据接口重要性执行失败关闭或受控降级,避免所有请求不受限制地进入数据库。
- 普通查询接口:可以根据业务容忍度设置短超时,并在必要时限制并发或临时拒绝部分请求。
- 关键业务接口:优先保证业务正确性,不能为了可用性而直接跳过必要的保护规则。
同时,需要设置 Redis 调用超时、连接池参数和必要的监控,避免限流组件自身成为新的性能瓶颈。
第六步:增加数据库侧保护
网关限流只能控制进入系统的请求量,不能保证每个请求的数据库开销相同。
因此,对于报表等重型查询接口,还需要结合数据库负载增加保护措施:
- 限制重型查询的并发数量。
- 合理配置数据库连接池,避免应用占满数据库连接。
- 对慢查询进行分析,优化 SQL、索引和查询范围。
- 对允许短暂陈旧的数据使用缓存,减少重复查询。
- 对长时间运行的导出任务采用异步任务和排队处理。
通过入口限流与数据库侧保护配合,才能同时控制请求速率、执行并发和数据库资源占用。
4. 上线验证与监控
上线前,先在测试环境进行压测,逐步增加请求量,观察限流是否生效,以及数据库负载是否处于可接受范围。
重点检查:
- 限流前后的请求量和 HTTP 429 比例。
- Redis 调用延迟、超时率和错误率。
- 业务服务的并发请求数和响应时间。
- 数据库 CPU、连接池使用率和慢查询数量。
- Redis 故障时,系统是否按照预定策略降级。
如果压测发现数据库仍然过载,应进一步调整接口限额、查询并发限制或 SQL,而不是只提高 Redis 的处理能力。
5. 方案优缺点
优点:
- 多个网关实例共享限流状态,适用于分布式部署。
- 可以根据用户、接口和业务类型配置不同规则。
- 通过 Lua 原子执行限流判断,避免并发计数竞争。
- 可以与网关入口保护、数据库并发控制和缓存结合使用。
缺点:
- 需要维护 Redis、Lua 脚本和限流规则。
- 请求需要访问 Redis,会增加一定的网络延迟。
- Redis 故障可能影响入口请求处理,需要明确降级策略。
- 速率限制无法完全解决慢 SQL 和数据库连接耗尽问题。
6. 方案总结
Redis + Lua 适合需要在多个应用实例之间共享限流额度的场景。工程实施时,先分析高压力接口并确定限流维度,再设计 Redis Key 和 Lua 脚本,随后接入 Gateway,最后结合故障降级、数据库并发控制和压测监控形成完整的保护链路。
对于数据库流量过大的问题,核心不是单纯限制请求次数,而是尽可能在请求进入业务服务之前拦截超额流量,并进一步控制真正消耗数据库资源的查询并发和执行成本。
相关文章
- 数据库流量过大:网关入口限流的四种工程方案:本文所属的方案总览,包含 Nginx 与云 API 网关等其余方案。
- 数据库流量过大:中间件防护与数据库保护:从整体链路看限流在数据库保护中的位置。
- 方案一:Spring Cloud Gateway 入口限流:基于 Gateway 内置限流组件的实现方式。