按 Enter 键跳转到正文

高并发系统设计-限流

高并发系统设计-限流

一、什么是限流

限流(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示例

1limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
2
3server {
4    location /api/ {
5        limit_req zone=api burst=20 nodelay;
6    }
7}

统一限制:

  • IP
  • Token
  • 用户
  • API

这是企业最常见的位置。

第三层:应用层

程序内部实现 如:

 1class RateLimitMiddleware
 2{
 3    public function process($request, $handler)
 4    {
 5        $key = "rate:" . $request->getUri()->getPath();
 6
 7        $count = Redis::incr($key);
 8
 9        if ($count == 1) {
10            Redis::expire($key, 1);
11        }
12
13        if ($count > 100) {
14            throw new HttpException(429, "Too Many Requests");
15        }
16
17        return $handler->handle($request);
18    }
19}

常用于:

  • 登录
  • 支付
  • 下单 等业务接口。

第四层:数据库保护

例如:

数据库连接数: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 请求

实现

 1$key = "rate:login:ip:1.1.1.1";
 2
 3$count = $redis->incr($key);
 4
 5if ($count == 1) {
 6    $redis->expire($key, 1);
 7}
 8
 9if ($count > 100) {
10    throw new Exception("Rate Limited");
11}

6.3 滑动窗口(Sliding Window)

原理

不再按“整秒”,而是统计:

最近 60 秒的请求数量

实现(Redis ZSet)

score = timestamp
value = request_id

php实现

 1$key = "rate:login:user:1";
 2
 3$now = microtime(true);
 4
 5$redis->zAdd($key, $now, uniqid());
 6
 7$redis->zRemRangeByScore($key, 0, $now - 60);
 8
 9$count = $redis->zCard($key);
10
11if ($count > 100) {
12    throw new Exception("Rate Limited");
13}

优点

  • 精确控制
  • 无突刺问题

缺点

  • Redis开销较大
  • 写入成本高

6.4 漏桶算法(Leaky Bucket)

原理

请求进入桶
   ↓
桶以固定速度流出

特点

  • 输出稳定
  • 不允许突发流量

适用场景

  • 消息队列消费
  • 流量整形
  • 下游保护

模拟实现(PHP)

 1class LeakyBucket
 2{
 3    private int $capacity = 100;
 4    private int $water = 0;
 5    private int $rate = 10; // 每秒流出
 6
 7    private int $lastTime;
 8
 9    public function __construct()
10    {
11        $this->lastTime = time();
12    }
13
14    public function allow(): bool
15    {
16        $now = time();
17        $diff = $now - $this->lastTime;
18
19        $this->water = max(0, $this->water - $diff * $this->rate);
20        $this->lastTime = $now;
21
22        if ($this->water < $this->capacity) {
23            $this->water++;
24            return true;
25        }
26
27        return false;
28    }
29}

6.5 令牌桶算法(Token Bucket) 主流选择

原理

系统持续生成 token:每秒生成 100 个 token

请求:拿 token 没有 token:拒绝

优势

  • 支持突发流量
  • 平滑控制
  • 企业标准方案

Redis实现(Lua)

 1local key = KEYS[1]
 2local rate = tonumber(ARGV[1])
 3local capacity = tonumber(ARGV[2])
 4local now = tonumber(ARGV[3])
 5
 6local data = redis.call("HMGET", key, "tokens", "time")
 7local tokens = tonumber(data[1])
 8local last = tonumber(data[2])
 9
10if tokens == nil then
11    tokens = capacity
12    last = now
13end
14
15local delta = math.max(0, now - last)
16tokens = math.min(capacity, tokens + delta * rate)
17
18if tokens < 1 then
19    return 0
20else
21    tokens = tokens - 1
22    redis.call("HMSET", key, "tokens", tokens, "time", now)
23    return 1
24end

PHP调用

 1$result = $redis->eval($lua, [
 2    "rate:api:user:1",
 3    100,
 4    200,
 5    time()
 6], 1);
 7
 8if ($result == 0) {
 9    throw new Exception("Rate Limited");
10}

发表评论