接口QPS限制与限流方案
限制某个接口服务的 QPS,本质上就是做限流。
但是实际项目里说“限制 QPS”,往往还不够准确。因为在真正落地时,通常还要先回答几个问题:
- 按什么维度限流
- 是单机服务还是多实例服务
- 是否允许突发流量
- 超限后是直接拒绝,还是排队等待
如果这些问题没有先想清楚,后面的实现方式也很容易选错。
这篇文章整理一下常见的限流方案,以及在实际项目中如何选择。
一、常见限流算法
几种常见限流算法示意:
1. 固定窗口限流
固定窗口的思路很简单,就是按时间窗口统计请求数。
例如:
- 1 秒内最多允许 100 次请求
实现方式通常是:
- 记录当前秒的请求次数
- 超过阈值就拒绝
优点:
- 实现简单
- 适合快速上线
缺点:
- 容易出现临界点突刺
比如上一秒末尾来了 100 次请求,下一秒开头又来了 100 次请求,那么虽然每个窗口都没超限,但实际瞬时流量可能接近 200。
适合场景:
- 简单后台接口
- 对限流精度要求不高的场景
2. 滑动窗口限流
滑动窗口可以理解为对固定窗口的优化。
它不会只看一个大窗口,而是把大窗口拆成多个小格子,然后统计最近一段时间内的总请求数。
例如:
- 1 秒限 100 次
- 拆成 10 个 100ms 小窗口
- 每次请求时统计最近 10 个格子的总和
优点:
- 比固定窗口更平滑
- 能有效减少临界点突刺
缺点:
- 实现复杂度比固定窗口高一些
适合场景:
- 对流量波动比较敏感的接口
- 希望限流更平滑的服务
3. 令牌桶
令牌桶是实际项目里非常常见的一种方案。
它的核心思路是:
- 系统按固定速率往桶里放令牌
- 请求来了先取令牌
- 取到令牌才允许通过
例如:
- 每秒产生 100 个令牌
- 桶最大容量 200
- 每个请求消耗 1 个令牌
优点:
- 能限制平均 QPS
- 同时允许一定程度的突发流量
缺点:
- 需要维护桶状态
- 实现复杂度比纯计数器略高
适合场景:
- 大多数常规接口限流
- 既要控平均流量,又不想完全抹平突发流量的场景
4. 漏桶
漏桶的思路是把请求先放进桶里,再按固定速率流出处理。
优点:
- 输出速度非常平稳
- 对后端服务的保护效果很直接
缺点:
- 突发请求容易排队
- 队列满了以后就会丢弃
- 对实时 HTTP 接口不一定友好
适合场景:
- 需要平滑消费的系统
- 更偏内部异步处理的链路
二、实际项目中,先明确“按什么维度限流”
做限流时,真正关键的往往不是算法本身,而是限流维度。
1. 按接口整体限流
例如:
/api/order/create最大 100 QPS
这种方式主要是保护服务总容量。
2. 按用户限流
例如:
- 每个用户每秒最多 5 次
这种方式适合防止单个用户刷接口。
3. 按 IP 限流
例如:
- 每个 IP 每秒最多 20 次
更适合未登录接口,比如公开查询接口、验证码接口。
4. 按客户端 ID / 设备 ID 限流
例如:
- 每台客户端每秒最多 3 次
适合桌面客户端、设备端或者 IoT 场景。
很多时候,一个系统不会只做一种维度,而是组合使用:
- 网关层按 IP 或接口总量限流
- 业务层按用户、设备、租户再做补充限流
三、超限后怎么处理
常见策略一般有三种。
1. 直接拒绝
这是最常见的做法,通常返回:
- HTTP 429 Too Many Requests
优点是实现简单,也符合大多数 HTTP 接口的预期。
2. 排队等待
这种方式更适合内部异步任务,不太适合普通实时接口。
因为对大多数 HTTP 请求来说,用户更关心的是快速失败还是快速成功,而不是无限等待。
3. 降级
例如:
- 返回缓存数据
- 返回默认值
- 返回简化结果
这种方式适合一些“可以退而求其次”的接口。
四、单机服务怎么做
如果当前只有一个服务实例,限流可以直接在应用内存中做。
例如在 Node.js 服务里,最简单的方式就是用计数器做一个固定窗口限流。
下面是一个“1 秒最多 10 次”的基础示例:
const express = require('express') |
这个实现很基础,但对于单实例的小项目来说已经够用了。
它本质上就是固定窗口限流。
单机固定窗口限流示意:
五、多实例服务怎么做
如果接口服务已经部署成多个实例,就不能只靠本地内存了。
原因很简单:
- 每个实例都会各自统计请求数
- 每台机器都可能各自放行一部分请求
- 最终整体 QPS 仍然可能超限
这时候常见的做法有三类:
- Redis 限流
- 网关层限流
- Nginx / Kong / APISIX / Envoy / Spring Cloud Gateway 等统一入口限流
1. Redis 限流
Redis 方案的核心是:
- 所有实例共用一份计数或桶状态
常见实现方式有:
INCR+EXPIRE- Lua 脚本保证原子性
- 用 Redis 实现固定窗口、滑动窗口或令牌桶
Redis 更适合:
- 多实例部署
- 需要统一限流状态
- 业务层需要按用户、设备、租户细分规则
2. 网关层限流
这是我更推荐的方式之一。
优点很明显:
- 不侵入业务代码
- 所有请求都会先经过网关
- 规则配置和监控更统一
常见限流维度包括:
- 整个接口
- IP
- 用户 ID
- 客户端 ID
- 租户
- token
网关层适合控整体流量和统一入口规则,业务层则适合处理更细的业务逻辑。
多实例统一限流示意:
六、Nginx 也可以做简单 QPS 限制
如果你的服务前面本身就有 Nginx,那么可以直接做一层基础限流。
例如按 IP 限流:
http { |
这段配置的含义是:
- 平均每秒 10 个请求
- 允许一定突发流量,突发上限 20
- 超过后开始限流
这种方式适合:
- 简单网关层防刷
- 未登录接口保护
- 低成本增加第一道流量防线
七、实际项目里应该怎么选
如果只看“能不能实现”,其实很多方式都能做。
真正要考虑的是:
- 部署架构
- 业务复杂度
- 是否有统一网关
- 是否需要动态调整规则
- 是否需要白名单
- 是否需要区分普通用户和 VIP 用户
1. 简单项目
建议:
- 单机服务:应用内存限流
- 一两台机器:Redis 限流
2. 正式生产环境
建议:
- 网关层限流为主
- 业务层做补充
- 核心接口再配合 Redis 或网关统一控制
3. 最常见组合
我自己更倾向这种组合:
- 网关层:控总量、控 IP
- 业务层:控用户、控设备、控特殊规则
这种分层方式比较清晰,也更容易维护。
八、如果是 Node.js 服务,我会怎么建议
如果你的服务本身是 Node.js,通常可以先按下面这个顺序考虑。
最小实现
- 单实例:直接在代码里做固定窗口或令牌桶
- 多实例:Redis + Lua 脚本
- 如果前面有 Nginx:先让 Nginx 做第一层基础限流
这样做的好处是:
- 先用最小成本解决问题
- 后面随着服务规模增长再逐步升级
九、最后几个关键点
做限流时,不要只盯着“QPS 限多少”这个数字。
还有这些问题也很重要:
- 限流维度是什么
- 是单机还是多实例
- 是否允许突发流量
- 超限后是拒绝还是排队
- 是否需要白名单
- 是否要区分普通用户和 VIP 用户
- 是否要支持动态调整阈值
很多线上问题并不是“没有限流”,而是“限流规则设计得不对”。
十、小结
如果只是想快速理解怎么限制一个接口服务的 QPS,可以先记住这几点:
- 固定窗口简单,但有临界突刺问题
- 滑动窗口更平滑
- 令牌桶最常用,兼顾平均速率和突发流量
- 漏桶更适合平滑消费
- 单机服务可以先在内存里做
- 多实例服务更适合 Redis 或网关统一限流
如果是实际生产环境,我通常更建议:
- 网关层控制总量和 IP
- 业务层补充用户、设备、租户等细粒度规则
这样整体会更稳,也更容易扩展。
参考文章