高并发系统设计-限流
一、什么是限流
限流(Rate Limiting)是指限制单位时间内允许进入系统的请求数量。
简单来说,就是:
控制流量进入系统的速度,而不是等系统崩溃之后再处理。
例如:
某接口限制:1000 Request / Second
如果一秒钟来了:1500 Request
那么:1000正常处理,500拒绝访问
通常返回:HTTP 429 Too Many Requests
限流的本质就是:
宁可拒绝一部分请求,也不要让整个系统崩溃。
二、为什么需要限流
因为系统资源是有限的。
例如:
服务器配置:
CPU:8 Core
Memory:16GB
MySQL连接池:100
Redis连接池:200
这些资源都存在上限。
假设数据库最大只能处理: 5000 QPS
如果突然来了:30000 QPS
没有限流:30000全部访问MySQL->连接池耗尽->CPU100%->慢SQL->超时->…->系统崩溃
有了限流:30000->Gateway->允许5000->25000直接拒绝->系统稳定
虽然部分用户会收到"系统繁忙"的提示,但系统整体仍然保持可用。
这正是限流存在的意义
三、典型应用场景
限流几乎存在于所有互联网系统中。
1、登录接口
例如:POST /login
限制:每个IP 5次/分钟
防止:
- 暴力破解密码
- 恶意攻击
- 短时间大量登录
2、短信验证码
3、支付接口
4、秒杀系统
5、开放API
四、限流的位置
限流通常不是只有一层。
一般有四层。
第一层:CDN
例如: Cloudflare->限制恶意流量
主要抵御:
- CC攻击
- DDoS
- Bot
第二层:Nginx / Gateway
例如: Client->Gateway->Application
nginx示例
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
server {
location /api/ {
limit_req zone=api burst=20 nodelay;
}
}
统一限制:
- IP
- Token
- 用户
- API
这是企业最常见的位置。
第三层:应用层
程序内部实现 如:
class RateLimitMiddleware
{
public function process($request, $handler)
{
$key = "rate:" . $request->getUri()->getPath();
$count = Redis::incr($key);
if ($count == 1) {
Redis::expire($key, 1);
}
if ($count > 100) {
throw new HttpException(429, "Too Many Requests");
}
return $handler->handle($request);
}
}
常用于:
- 登录
- 支付
- 下单 等业务接口。
第四层:数据库保护
例如:
数据库连接数:100
如果:
连接100->拒绝新连接
虽然这不是严格意义上的限流,但属于最后一道保护措施。
五、限流维度
通常不会只有一种限流方式。
常见维度如下:
| 限流方式 | 示例 |
|---|---|
| IP限流 | 一个IP一分钟100次 |
| 用户限流 | 每个用户100次/分钟 |
| Token限流 | 每个AccessToken限制 |
| 接口限流 | /login 每秒100次 |
| 应用限流 | AppKey限制 |
| 地区限流 | 海外地区单独限制 |
| 全局限流 | 整个平台10000 QPS |
六、限流算法概览
6.1 四种主流限流算法
目前最常见的四种算法如下:
| 算法 | 是否推荐 | 特点 |
|---|---|---|
| 固定窗口 | ★★☆☆☆ | 实现简单,存在流量突刺 |
| 滑动窗口 | ★★★★☆ | 精确统计,请求稍复杂 |
| 漏桶算法 | ★★★★☆ | 输出稳定,适合流量整形 |
| 令牌桶算法 | ★★★★★ | 企业使用最广,兼顾稳定与突发 |
| 其中: |
- 固定窗口:实现最简单,适合低并发系统。
- 滑动窗口:统计更准确,适用于业务限流。
- 漏桶算法:控制输出速率,适合网关和流量整形。
- 令牌桶算法:支持突发流量,是现代网关和框架最常用的方案。
6.2 固定窗口(Fixed Window)
原理
时间:10:00:00 ~ 10:00:01
计数器:count++
超过限制:
count > limit → 拒绝
问题(非常重要)
窗口边界会产生突刺:
10:00:00.999 → 100次
10:00:01.000 → 100次
1ms 内 200 请求
实现
$key = "rate:login:ip:1.1.1.1";
$count = $redis->incr($key);
if ($count == 1) {
$redis->expire($key, 1);
}
if ($count > 100) {
throw new Exception("Rate Limited");
}
6.3 滑动窗口(Sliding Window)
原理
不再按“整秒”,而是统计:
最近 60 秒的请求数量
实现(Redis ZSet)
score = timestamp
value = request_id
php实现
$key = "rate:login:user:1";
$now = microtime(true);
$redis->zAdd($key, $now, uniqid());
$redis->zRemRangeByScore($key, 0, $now - 60);
$count = $redis->zCard($key);
if ($count > 100) {
throw new Exception("Rate Limited");
}
优点
- 精确控制
- 无突刺问题
缺点
- Redis开销较大
- 写入成本高
6.4 漏桶算法(Leaky Bucket)
原理
请求进入桶
↓
桶以固定速度流出
特点
- 输出稳定
- 不允许突发流量
适用场景
- 消息队列消费
- 流量整形
- 下游保护
模拟实现(PHP)
class LeakyBucket
{
private int $capacity = 100;
private int $water = 0;
private int $rate = 10; // 每秒流出
private int $lastTime;
public function __construct()
{
$this->lastTime = time();
}
public function allow(): bool
{
$now = time();
$diff = $now - $this->lastTime;
$this->water = max(0, $this->water - $diff * $this->rate);
$this->lastTime = $now;
if ($this->water < $this->capacity) {
$this->water++;
return true;
}
return false;
}
}
6.5 令牌桶算法(Token Bucket) 主流选择
原理
系统持续生成 token:每秒生成 100 个 token
请求:拿 token 没有 token:拒绝
优势
- 支持突发流量
- 平滑控制
- 企业标准方案
Redis实现(Lua)
local key = KEYS[1]
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local data = redis.call("HMGET", key, "tokens", "time")
local tokens = tonumber(data[1])
local last = tonumber(data[2])
if tokens == nil then
tokens = capacity
last = now
end
local delta = math.max(0, now - last)
tokens = math.min(capacity, tokens + delta * rate)
if tokens < 1 then
return 0
else
tokens = tokens - 1
redis.call("HMSET", key, "tokens", tokens, "time", now)
return 1
end
PHP调用
$result = $redis->eval($lua, [
"rate:api:user:1",
100,
200,
time()
], 1);
if ($result == 0) {
throw new Exception("Rate Limited");
}