接口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')
const app = express()

let count = 0
let currentSecond = Math.floor(Date.now() / 1000)

app.use('/api/test', (req, res, next) => {
const nowSecond = Math.floor(Date.now() / 1000)

if (nowSecond !== currentSecond) {
currentSecond = nowSecond
count = 0
}

count++

if (count > 10) {
return res.status(429).json({
code: 429,
message: '请求过多,请稍后再试'
})
}

next()
})

app.get('/api/test', (req, res) => {
res.json({ ok: true })
})

app.listen(3000)

这个实现很基础,但对于单实例的小项目来说已经够用了。

它本质上就是固定窗口限流。

单机固定窗口限流示意:

五、多实例服务怎么做

如果接口服务已经部署成多个实例,就不能只靠本地内存了。

原因很简单:

  • 每个实例都会各自统计请求数
  • 每台机器都可能各自放行一部分请求
  • 最终整体 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 {
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

server {
location /api/test {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://backend;
}
}
}

这段配置的含义是:

  • 平均每秒 10 个请求
  • 允许一定突发流量,突发上限 20
  • 超过后开始限流

这种方式适合:

  • 简单网关层防刷
  • 未登录接口保护
  • 低成本增加第一道流量防线

七、实际项目里应该怎么选

如果只看“能不能实现”,其实很多方式都能做。

真正要考虑的是:

  • 部署架构
  • 业务复杂度
  • 是否有统一网关
  • 是否需要动态调整规则
  • 是否需要白名单
  • 是否需要区分普通用户和 VIP 用户

1. 简单项目

建议:

  • 单机服务:应用内存限流
  • 一两台机器:Redis 限流

2. 正式生产环境

建议:

  • 网关层限流为主
  • 业务层做补充
  • 核心接口再配合 Redis 或网关统一控制

3. 最常见组合

我自己更倾向这种组合:

  • 网关层:控总量、控 IP
  • 业务层:控用户、控设备、控特殊规则

这种分层方式比较清晰,也更容易维护。

八、如果是 Node.js 服务,我会怎么建议

如果你的服务本身是 Node.js,通常可以先按下面这个顺序考虑。

最小实现

  • 单实例:直接在代码里做固定窗口或令牌桶
  • 多实例:Redis + Lua 脚本
  • 如果前面有 Nginx:先让 Nginx 做第一层基础限流

这样做的好处是:

  • 先用最小成本解决问题
  • 后面随着服务规模增长再逐步升级

九、最后几个关键点

做限流时,不要只盯着“QPS 限多少”这个数字。

还有这些问题也很重要:

  • 限流维度是什么
  • 是单机还是多实例
  • 是否允许突发流量
  • 超限后是拒绝还是排队
  • 是否需要白名单
  • 是否要区分普通用户和 VIP 用户
  • 是否要支持动态调整阈值

很多线上问题并不是“没有限流”,而是“限流规则设计得不对”。

十、小结

如果只是想快速理解怎么限制一个接口服务的 QPS,可以先记住这几点:

  • 固定窗口简单,但有临界突刺问题
  • 滑动窗口更平滑
  • 令牌桶最常用,兼顾平均速率和突发流量
  • 漏桶更适合平滑消费
  • 单机服务可以先在内存里做
  • 多实例服务更适合 Redis 或网关统一限流

如果是实际生产环境,我通常更建议:

  • 网关层控制总量和 IP
  • 业务层补充用户、设备、租户等细粒度规则

这样整体会更稳,也更容易扩展。

参考文章