为什么要限流?常见的限流算法有哪些?

2024年 6月 5日 93.7k 0

实际开发中,当业务流量过大时,为了保护下游服务,我们通常会做一些预防性的工作,今天我们就一起来聊聊限流!

为什么要限流?常见的限流算法有哪些?-1

一、为什么需要限流?

在实际应用中,每个系统或者服务都有其处理能力的极限(瓶颈),即便是微服务中有集群和分布式的夹持,也不能保证系统能应对任何大小的流量,因此,系统为了自保,需要对处理能力范围以外的流量进行“特殊照顾”(比如,丢弃请求或者延迟处理),从而避免系统卡死、崩溃或不可用等情况,保证系统整体服务可用。

二、限流算法

1.令牌桶算法

令牌桶算法(Token Bucket Algorithm)是计算机网络和电信领域中常用的一种简单方法,用于流量整形和速率限制。它旨在控制系统在某个时间段内可以发送或接收的数据量,确保流量符合指定的速率。

令牌桶算法的核心思路:系统按照固定速度往桶里加入令牌,如果桶满则停止添加。当有请求到来时,会尝试从桶里拿走一个令牌,取到令牌才能继续进行请求处理,没有令牌就拒绝服务。示意图如下:

为什么要限流?常见的限流算法有哪些?-1

令牌桶法的几个特点:

  • 令牌桶容量固定,即系统的处理能力阈值
  • 令牌放入桶内的速度固定
  • 令牌从桶内拿出的速度根据实际请求量而定,每个请求对应一个令牌
  • 当桶内没有令牌时,请求进入等待或者被拒绝

令牌桶算法主要用于应对突发流量的场景,在 Java语言中使用最多的是 Google的 Guava RateLimiter,下面举几个例子来说明它是如何应对突发流量:

示例1

import java.util.concurrent.TimeUnit;
public class RateLimit {

  public static void main(String[] args) {
    RateLimiter limiter = RateLimiter.create(5); // 每秒创建5个令牌
    System.out.println("acquire(5), wait " + limiter.acquire(5) + " s"); // 全部取走 5个令牌
    System.out.println("acquire(1), wait " + limiter.acquire(1) + " s");// 获取1个令牌
    boolean result = limiter.tryAcquire(1, 0, TimeUnit.SECONDS); // 尝试获取1个令牌,获取不到则直接返回
    System.out.println("tryAcquire(1), result: " + result);
  }
}

示例代码运行结果如下:

acquire(5), wait 0.0 s
acquire(1), wait 0.971544 s
tryAcquire(1), result: false

桶中共有 5个令牌,acquire(5)返回0 代表令牌充足无需等待,当桶中令牌不足,acquire(1)等待一段时间才获取到,当令牌不足时,tryAcquire(1)不等待直接返回。

示例2

import com.google.common.util.concurrent.RateLimiter;
public class RateLimit {
    public static void main(String[] args) {
        RateLimiter limiter = RateLimiter.create(5);
        System.out.println("acquire(10), wait " + limiter.acquire(10) + " s");
        System.out.println("acquire(1), wait " + limiter.acquire(1) + " s");
    }
}

示例代码运行结果如下:

acquire(10), wait 0.0 s
acquire(1), wait 1.974268 s

桶中共有 5个令牌,acquire(10)返回0,和示例似乎有点冲突,其实,这里返回0 代表应对了突发流量,但是 acquire(1)

却等待了 1.974268秒,这代表 acquire(1)的等待是时间包含了应对突然流量多出来的 5个请求,即 1.974268 = 1 + 0.974268。

为了更好的验证示例2的猜想,我们看示例3:

示例3

import com.google.common.util.concurrent.RateLimiter;
import java.util.concurrent.TimeUnit;
public class RateLimit {
    public static void main(String[] args) throws InterruptedException {
        RateLimiter limiter = RateLimiter.create(5);
        System.out.println("acquire(5), wait " + limiter.acquire(5) + " s");
        TimeUnit.SECONDS.sleep(1);
        System.out.println("acquire(5), wait " + limiter.acquire(5) + " s");
        System.out.println("acquire(1), wait " + limiter.acquire(1) + " s");
    }
}

示例代码运行结果如下:

acquire(5), wait 0.0 s
acquire(5), wait 0.0 s
acquire(1), wait 0.966104 s

桶中共有 5个令牌,acquire(5)返回0 代表令牌充足无需等待,接着睡眠 1s,这样系统又可以增加5个令牌,因此,再次 acquire(5)令牌充足返回0 无需等待,acquire(1)需要等待一段时间才能获取令牌。

2.漏桶算法

漏桶算法(Leaky Bucket Algorithm)的核心思路是:水(请求)进入固定容量的漏桶,漏桶的水以固定的速度流出,当水流入漏桶的速度过大导致漏桶满而直接溢出,然后拒绝请求。示意图如下:

为什么要限流?常见的限流算法有哪些?-2

下面为一个 Java版本的漏桶算法示例:

import java.util.concurrent.*;
public class LeakyBucket {
    private final int capacity; // 桶的容量
    private final int rate;     // 出水速率
    private int water;          // 漏斗中的水量
    private long lastLeakTime;  // 上一次漏水的时间

    public LeakyBucket(int capacity, int rate) {
        this.capacity = capacity;
        this.rate = rate;
        this.water = 0;
        this.lastLeakTime = System.currentTimeMillis();
    }

    public synchronized boolean allowRequest(int tokens) {
        leak(); // 漏水
        if (water + tokens  windowSizeMs) {
            counter.set(0);
            lastResetTime = currentTime;
        }
        // 检查计数器是否超过了限流阈值
        return counter.incrementAndGet() = limit) {
            return false; // 超过限流阈值,拒绝请求
        } else {
            window[pointer].incrementAndGet(); // 记录新的请求
            return true; // 允许请求
        }
    }

    public static void main(String[] args) {
        SlidingWindowRateLimiter rateLimiter = new SlidingWindowRateLimiter(10, 1000, 10); // 每秒最多处理10个请求
        for (int i = 0; i < 20; i++) {
            if (rateLimiter.allowRequest()) {
                System.out.println("允许请求 " + (i + 1));
            } else {
                System.out.println("限流,拒绝请求 " + (i + 1));
            }
            try {
                Thread.sleep(100); // 模拟请求间隔
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

5.Redis + Lua分布式限流

Redis + Lua属于分布式环境下的限流方案,主要利用的是Lua在 Redis中运行能保证原子性。如下示例为一个简单的Lua限流脚本:

local key = KEYS[1]
local limit = tonumber(ARGV[1])
local current = tonumber(redis.call('get', key) or "0")

if current + 1 > limit then
    return 0
else
    redis.call("INCRBY", key, 1)
    redis.call("EXPIRE", key, 1)
    return 1
end

脚本解释:

  • KEYS[1]:限流的键名,注意,在Lua中,下角标是从 1开始
  • ARGV[1]:限流的最大值
  • redis.call(‘get’, key):获取当前限流计数。
  • redis.call(‘INCRBY’, key, 1):增加限流计数。
  • redis.call(‘EXPIRE’, key, 1):设置键的过期时间为 1 秒。

6.三方限流工具

当我们自己无法实现比较好的限流方案时,成熟的三方框架就是我们比较好的选择,下面列出两个 Java语言比较优秀的框架。

(1) resilience4j

resilience4j 是一个轻量级的容错库,提供了限流、熔断、重试等功能。限流模块 RateLimiter 提供了灵活的限流配置,其优点如下:

  • 集成了多种容错机制
  • 支持注解方式配置
  • 易于与 Spring Boot集成

(2) Sentinel

Sentinel 是阿里巴巴开源的一个功能全面的流量防护框架,提供限流、熔断、系统负载保护等多种功能。其优点如下:

  • 功能全面,适用于多种场景
  • 强大的监控和控制台
  • 与 Spring Cloud 深度集成

三、总结

本文讲述了以下几种限流方式:

  • 计数器
  • 滑动窗口
  • 漏桶
  • 令牌桶
  • Redis + Lua 分布式限流
  • 三方限流工具

上面的限流方式,主要是针对服务器进行限流,除此之外,我们也可以对客户端进行限流, 比如验证码,答题,排队等方式。

另外,我们也会在一些中间件上进行限流,比如Apache、Tomcat、Nginx等。

在实际的开发中,限流场景略有差异,限流的维度也不一样,比如,有的场景需要根据请求的 URL来限流,有的会对 IP地址进行限流、另外,设备ID、用户ID 也是限流常用的维度,因此,我们需要结合真实业务场景灵活的使用限流方案。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论