常用的几种限流算法

1,427 阅读3分钟

计数器限流算法

限制在某个时间段内处理请求的次数,比如 1 分钟内只能处理 100 次请求 第一分钟的最后 10 秒和第二分种的开始 10 秒,它们都在各自 1 分钟周期内同时满足访问 100 次,那么它们在这个 20 秒的时间段就访问了 200 次超过了限流负荷,服务可能就挂了

static final int limit = 100;

// 1 秒种内只能处理 100 个请求
public boolean access(String key) {
	int val = jedis.incr(key);
    if (val == 1) {
    	jedis.pexpire(key, 1000);
        return true;
    }
    return val <= limit;
}

漏桶限流算法

漏桶以一定的速率向外漏水,服务器就以这个速率来处理请求,通过这种方式就能避免服务器承载请求过量,当遭大规模遇突发请求的时候,进入率大于出水率,漏桶水位会不断上升,当水位溢出的时候触发拒绝策略

它的缺点是无法处理突发的大量的并发请求,服务器只能以漏水的恒定速率去处理请求,如果漏水速率大于等于进水速率那么,漏桶永远都不会满

Redis-cell 提供了漏桶限流的实现,通过再项目中引入 Redis-cell 可以实现单机或者分布式限流

// 用户回复接口,每 60 秒只能只能调用 30 次,漏洞容量为 100
> cl.throttle user:reply 100 30 60

令牌桶限流算法

以一定的速率往桶中放入令牌,桶满了后丢弃令牌

服务器处理客户端请求的时候先从桶中获取令牌,获取成功处理请求,获取失败则采用拒绝策略处理请求

采用 Guava 来实现令牌桶算法

// 每秒生成 50 个令牌
final static RateLimiter rateLimiter = RateLimiter.create(50, 1, TimeUnit.SECONDS);

public boolean access() {
	boolean acquired = rateLimiter.tryAcquire();
    if (!acquired) {
    	// 令牌桶空了,被限流了
    	return false;
    }
    return true;
}

滑动窗口限流算法

我们通过窗口间隔时间定时清空上一个窗口的请求数量,然后切换到下一个窗口,通过取余的方式循环利用一个数组来实现

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicInteger;

public class SlidingWindowLimiter {

    // 默认美秒,允许通过请求的总数
    private int limitRequestTotal = 60;

    // 窗口数量
    private int windowNum = 5;

    // 窗口之间的时间间隔 1000MS / 10 窗口,一个窗口 100MS
    private int interval = 1000 / windowNum;

    // 每个窗口能够通过的请求数量
    private int windowRequestLimit = limitRequestTotal / windowNum;

    // 多少个窗口
    private Window[] windows = new Window[windowNum];

    // 当前窗口
    private int currentIndex = 0;

    // 当前总的滑动窗口请求总数
    private AtomicInteger currentRequestTotal = new AtomicInteger(0);

    // 默认参数每分钟允许请求 60 次,5 个窗口,每个窗口 200MS 允许请求,每个窗口允许请求上限 20 次
    public SlidingWindowLimiter() {
    }

    public SlidingWindowLimiter(int secondPer, int windowNum, int limitRequestTotal) {

        this.windowNum = windowNum;
        this.interval = (secondPer * 1000) / windowNum;
        this.limitRequestTotal = limitRequestTotal;
        this.windowRequestLimit = limitRequestTotal / windowNum;

        for (int i = 0; i < windows.length; i++) {
            windows[i] = new Window();
        }

        Timer timer = new Timer();
        // 每隔一个窗口时间,清空上一个窗口积累的请求数
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {
                // 下一个窗口
                currentIndex = (currentIndex + 1) % windowNum;
                // 上一个窗口的请求总数
                int beforeCount = windows[currentIndex].requestCount.get();
                currentRequestTotal.addAndGet(-beforeCount);
            }
        };
        timer.schedule(timerTask, interval, interval);
    }

    public boolean doLimit() {
        if (currentRequestTotal.get() > limitRequestTotal) {
            return false;
        }
        if (windows[currentIndex].requestCount.incrementAndGet() > windowRequestLimit) {
            return false;
        }
        // 请求总数加 1
        currentRequestTotal.incrementAndGet();
        return true;
    }

    class Window {

        private AtomicInteger requestCount;

        public Window() {
            this.requestCount = new AtomicInteger(0);
        }
    }
}

测试代码

    @Test
    public void testSlidingWindowLimiter() throws InterruptedException {
        // 每秒允许发起 100 次请求,5 个窗口,每个窗口 200 MS,每个窗口允许访问 20 次
        SlidingWindowLimiter limiter = new SlidingWindowLimiter(1,5,100);
        for (int i = 0; i < 100; i++) {
            boolean access = limiter.doLimit();
            if (access) {
                System.out.println(i +"访问成功");
            } else {
                System.out.println(i+ "被限流了");
            }
            Thread.sleep(5);
        }
    }

从输出结果来看,当 i = 19 的时候,表示发起了 20 个请求都是成功的,当 i = 20 的时候,sleep 了 100MS + 代码处理时间也就是 100 多 MS(小于 200),所以后面几十毫秒处理都会失败,然后到窗口切换时间后,清空上一个窗口请求数量,下一个窗口就能继续处理请求

可以看出滑动窗口限流算法,跟窗口数量关系密切,窗口数量越多,窗口间隔时间越短,就能够更加及时的处理新到来的请求,统计限流就能更加精确