连ChatGPT都会搞错限流算法你不来看看吗

293 阅读5分钟

记录今天研究的两种限流算法,令牌桶算法和漏桶算法,他们是两种非常常用的限流算法,比如Guava的RateLimiter限流组件,它是基于令牌桶算法实现的

令牌桶算法

设计思想

有一个装令牌的桶,我以恒定的速率往桶里加入令牌,当请求来时从桶里获取令牌,若成功获取令牌则放行,若桶内没有可获取的令牌则获取失败被限流。

实现思路

可以开启一个线程定时往桶里加令牌,但是有另一种方式更轻量级:记录上次令牌刷新的时间oldtime,当请求来时计算当前时间和oldtime的时间差*速率就可以这段时间产生的令牌数量,这种实现方式也很常见,比如在redis中判断key是否过期也是采用这种方式

  1. 计算当前时间和上次令牌刷新的时间oldtime的时间差*速率就可以这段时间产生的令牌数量,加上之前剩余的令牌得到当前桶内的令牌
  2. 如果令牌大于0则放行本次请求,并消耗本次令牌,如果令牌等于0则本次请求被限流

代码:

import lombok.extern.slf4j.Slf4j;
​
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
​
@Slf4j
public class TokenBucket {
    private final int capacity;  // 令牌桶容量
    private final int rate;  // 令牌生成速率(每秒生成的令牌数)
    private int tokens;  // 当前令牌数量
    private long lastRefillTime;  // 上次令牌生成时间
​
    public TokenBucket(int capacity, int rate) {
        this.capacity = capacity;
        this.rate = rate;
        this.tokens = 0;
        this.lastRefillTime = System.currentTimeMillis();
    }
​
    public synchronized boolean allowRequest() {
        refillTokens();
​
        // 如果令牌数量小于1,则拒绝请求
        if (tokens < 1) {
            return false;
        }
​
        // 令牌数量减1
        tokens--;
        return true;
    }
​
    private void refillTokens() {
        long currentTime = System.currentTimeMillis();
        long elapsedTime = currentTime - lastRefillTime;
        int tokensToAdd = (int) (elapsedTime / 1000 * rate);
​
        // 令牌数量不能超过容量
        tokens = Math.min(tokens + tokensToAdd, capacity);
        lastRefillTime = currentTime;
    }
​
    public static void main(String[] args) throws InterruptedException {
        TokenBucket tokenBucket = new TokenBucket(10, 2);  // 令牌桶容量为10,每秒生成2个令牌
        // 模拟请求 创建一个最大线程数5个线程的线程池
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5,5,0, TimeUnit.MINUTES,new ArrayBlockingQueue<Runnable>(10));
​
        // 每秒有5个请求,连续5轮,看最终有多少能通过
        for (int k = 0; k <5 ; k++) {
            Thread.sleep(1000);
            for (int i = 0; i < 5; i++) {
                int j =i;
                poolExecutor.execute(()->{
                    if (tokenBucket.allowRequest()) {
                        log.info("Request " + (j + 1) + ": Allowed");
                    }else
                        log.info("Request " + (j + 1) + ": Rejected");
                });
            }
        }
    }
}

结果

第一轮:
21:54:18.031 [pool-1-thread-2] INFO com.netty.demo.chatroom.TokenBucket - Request 2: Rejected
21:54:18.031 [pool-1-thread-3] INFO com.netty.demo.chatroom.TokenBucket - Request 3: Allowed
21:54:18.031 [pool-1-thread-4] INFO com.netty.demo.chatroom.TokenBucket - Request 4: Rejected
21:54:18.031 [pool-1-thread-1] INFO com.netty.demo.chatroom.TokenBucket - Request 1: Allowed
21:54:18.031 [pool-1-thread-5] INFO com.netty.demo.chatroom.TokenBucket - Request 5: Rejected
  
第二轮
21:54:19.030 [pool-1-thread-5] INFO com.netty.demo.chatroom.TokenBucket - Request 2: Allowed
21:54:19.031 [pool-1-thread-3] INFO com.netty.demo.chatroom.TokenBucket - Request 3: Rejected
21:54:19.031 [pool-1-thread-5] INFO com.netty.demo.chatroom.TokenBucket - Request 5: Rejected
21:54:19.030 [pool-1-thread-2] INFO com.netty.demo.chatroom.TokenBucket - Request 1: Allowed
21:54:19.031 [pool-1-thread-1] INFO com.netty.demo.chatroom.TokenBucket - Request 4: Rejected
  
第三轮
21:54:20.036 [pool-1-thread-4] INFO com.netty.demo.chatroom.TokenBucket - Request 1: Allowed
21:54:20.036 [pool-1-thread-3] INFO com.netty.demo.chatroom.TokenBucket - Request 2: Allowed
21:54:20.037 [pool-1-thread-1] INFO com.netty.demo.chatroom.TokenBucket - Request 5: Rejected
21:54:20.036 [pool-1-thread-5] INFO com.netty.demo.chatroom.TokenBucket - Request 3: Rejected
21:54:20.037 [pool-1-thread-2] INFO com.netty.demo.chatroom.TokenBucket - Request 4: Rejected
  
第四轮
21:54:21.040 [pool-1-thread-5] INFO com.netty.demo.chatroom.TokenBucket - Request 4: Rejected
21:54:21.040 [pool-1-thread-1] INFO com.netty.demo.chatroom.TokenBucket - Request 3: Rejected
21:54:21.040 [pool-1-thread-4] INFO com.netty.demo.chatroom.TokenBucket - Request 1: Allowed
21:54:21.040 [pool-1-thread-2] INFO com.netty.demo.chatroom.TokenBucket - Request 5: Rejected
21:54:21.040 [pool-1-thread-3] INFO com.netty.demo.chatroom.TokenBucket - Request 2: Allowed
  
第五轮
21:54:22.046 [pool-1-thread-4] INFO com.netty.demo.chatroom.TokenBucket - Request 3: Rejected
21:54:22.046 [pool-1-thread-2] INFO com.netty.demo.chatroom.TokenBucket - Request 4: Rejected
21:54:22.046 [pool-1-thread-5] INFO com.netty.demo.chatroom.TokenBucket - Request 1: Allowed
21:54:22.046 [pool-1-thread-4] INFO com.netty.demo.chatroom.TokenBucket - Request 5: Rejected
21:54:22.046 [pool-1-thread-1] INFO com.netty.demo.chatroom.TokenBucket - Request 2: Allowed

特点:

  1. 4个重要组成要素:容量、速率、时间、令牌数量
  2. 设计思想:通过控制生成令牌的速率间接控制流量,他希望控制的使一段时间内的平均流量,允许某些时刻出现大流量的情况
  3. 由于令牌可以堆积,所以在令牌桶内令牌充足时突然有大量的请求打来也可以得到处理

漏桶算法

设计思想

相当于是一个漏斗,当请求到来时将请求装入漏斗,通过控制出口的速率来控制进入系统的流量。

注意:

他的设计思想是在任意时刻都只允许出口速率大小的流量进入系统,不允许任意时刻超过出口流量

现在网上很多漏斗算法的实现都违背了漏斗算法的设计思想,包括从chatgpt得到的答案也是如此,最后得到的漏斗算法实现其实和令牌桶算法没有任何区别

比如举个错误的例子:

/**
     * 每秒处理数(出水率)
     */
    private long rate;
​
    /**
     *  当前剩余水量
     */
    private long currentWater;
​
    /**
     * 最后刷新时间
     */
    private long refreshTime;
​
    /**
     * 桶容量
     */
    private long capacity;
​
    /**
     * 漏桶算法
     * @return
     */
    boolean leakybucketLimitTryAcquire() {
        long currentTime = System.currentTimeMillis();  //获取系统当前时间
        long outWater = (currentTime - refreshTime) / 1000 * rate; //流出的水量 =(当前时间-上次刷新时间)* 出水率
        long currentWater = Math.max(0, currentWater - outWater); // 当前水量 = 之前的桶内水量-流出的水量
        refreshTime = currentTime; // 刷新时间
​
        // 当前剩余水量还是小于桶的容量,则请求放行   ERROR、ERROR、ERROR
        if (currentWater < capacity) {
            currentWater++;
            return true;
        }
        
        // 当前剩余水量大于等于桶的容量,限流
        return false;
    }

上述算法在请求进来时判断当前桶是否装满,如果没装满则放行,实际上是错的很离谱

我们举个例子,当我们有一个容量为10的桶,希望控制速率为2QPS,现在桶是空的,有10个请求进来

上述算法会将10个请求都放行,也就是说当前时刻的QPS达到了10QPS远远大于我们期望的2QPS(这样的实现其实就是另一种令牌桶算法了)

漏桶算法则只会将前两个请求放行,剩余8个请求留在桶中等待下一次放行时刻到来则再放行2个请求,从而达到任意时刻都不超过2QPS

对比

令牌桶算法是希望控制一段时间内流入系统的平均速率,允许某些时刻出现不超过最大容量的大流量出现

漏桶算法是希望控制任意时刻流入系统的流量都不要超过预定速率

很多人说令牌桶算法允许某些时刻出现大流量的特点是其优点,但是我觉得两种算法都有其应用场景,应该根据具体的业务需求去选择何种算法