常见限流算法介绍

132 阅读7分钟

以下简单介绍几种常见的限流算法。

1、固定时间窗口算法

描述: 固定时间窗口算法是一种简单的限流算法,它将时间划分为固定大小的窗口,在每个窗口内限制请求的数量。超过数量就拒绝或者排队,等下一个时间段进入。

image.png

优点: 算法简单易懂,适用于固定间隔内的请求控制。

缺点: 无法处理跨时间窗口的请求控制问题,可能会导致突发请求超出限制。

适用场景:  适合对请求进行固定间隔控制的场景,如限制每秒、每分钟的请求数量。

Java语言实现参考:

import java.util.concurrent.*;

public class FixedWindowRateLimiter {
    private final int limit; // 每个时间窗口内允许的请求数量
    private final long windowSizeMillis; // 时间窗口的大小,以毫秒为单位
    private long lastWindowStart;
    private int count;

    public FixedWindowRateLimiter(int limit, long windowSizeMillis) {
        this.limit = limit;
        this.windowSizeMillis = windowSizeMillis;
        this.lastWindowStart = System.currentTimeMillis();
        this.count = 0;
    }

    public synchronized boolean allowRequest() {
        long now = System.currentTimeMillis();
        if (now - lastWindowStart > windowSizeMillis) {
            lastWindowStart = now;
            count = 0;
        }
        if (count < limit) {
            count++;
            return true;
        }
        return false;
    }

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

2、滑动时间窗口算法

描述: 滑动时间窗口算法通过动态调整时间窗口的起止时间来控制请求流量,避免了固定窗口算法的刚性限制。即将每一次请求的到来时间点作为统计时间窗的终点,起点则是终点向前推时间窗长度的时间点。

image.png

优点: 可以动态地适应请求的变化,更灵活。

缺点: 可能会导致大量的时间窗口重叠计算,增加系统负载。

适用场景:  需要更灵活的请求流量控制,能够处理短时间内突发的请求。

Java语言实现参考:

import java.util.concurrent.*;

public class SlidingWindowRateLimiter {
    private final int limit; // 滑动窗口内允许的最大请求数
    private final long windowSizeMillis; // 滑动窗口的大小,以毫秒为单位
    private final ConcurrentLinkedDeque<Long> requestTimes; // 保存请求的时间戳

    public SlidingWindowRateLimiter(int limit, long windowSizeMillis) {
        this.limit = limit;
        this.windowSizeMillis = windowSizeMillis;
        this.requestTimes = new ConcurrentLinkedDeque<>();
    }

    public synchronized boolean allowRequest() {
        long now = System.currentTimeMillis();
        requestTimes.addLast(now);
        
        // 移除超出窗口的请求时间戳
        while (!requestTimes.isEmpty() && now - requestTimes.getFirst() > windowSizeMillis) {
            requestTimes.removeFirst();
        }

        // 判断当前窗口内的请求数是否超过限制
        return requestTimes.size() <= limit;
    }

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

3、样本窗口算法

描述: 基于滑动时间窗口算法优化,1个滑动窗口划分多个样本窗口,每个样本窗口在到达终点时间时,会统计本样本窗口中的流量数据并且记录下来。当一个请求达到时,会统计当前请求时间点所在的样本窗口中的流量数据,然后在获取当前请求时间的样本窗口以外的同一个滑动窗口中的样本窗口的统计数据,进行求和,如果没有超出阈值,则通过,否则就会被限流。

image.png

image.png

优点: 样本窗口算法能够更精确地根据历史请求情况调整限制,有效应对请求流量的变化。

缺点: 实现复杂度较高,需要对历史请求数据进行有效的采样和分析。

适用场景:  适用于需要更精确流量控制的场景,能够动态调整请求限制以应对不同时间段内的请求变化。

Java语言实现参考:

import java.util.ArrayDeque;
import java.util.Deque;

public class SampleWindowAlgorithm {

    private int windowSize; // 时间窗口大小,单位:毫秒
    private int sampleSize; // 每个时间窗口内采样的请求数量
    private Deque<Integer> samples; // 存储每个时间窗口内的样本请求数量

    public SampleWindowAlgorithm(int windowSize, int sampleSize) {
        this.windowSize = windowSize;
        this.sampleSize = sampleSize;
        this.samples = new ArrayDeque<>();
    }

    // 处理一个新的请求,返回是否允许处理该请求
    public boolean allowRequest() {
        long currentTime = System.currentTimeMillis();
        cleanOldSamples(currentTime); // 清理过期的样本数据

        if (samples.size() >= sampleSize) {
            // 如果样本已满,根据样本情况决定是否允许新请求
            int totalRequests = samples.stream().mapToInt(Integer::intValue).sum();
            int average = totalRequests / samples.size();
            return average < sampleSize; // 如果平均请求数小于采样数量,允许处理新请求
        } else {
            // 如果样本未满,直接允许处理新请求
            return true;
        }
    }

    // 添加一个新的请求样本
    public void addSample() {
        samples.addLast(1); // 在实际应用中可能需要根据请求的实际量调整
        cleanOldSamples(System.currentTimeMillis());
    }

    // 清理过期的样本数据
    private void cleanOldSamples(long currentTime) {
        while (!samples.isEmpty() && currentTime - samples.peekFirst() >= windowSize) {
            samples.removeFirst();
        }
    }

    public static void main(String[] args) {
        SampleWindowAlgorithm sampleWindow = new SampleWindowAlgorithm(10000, 5); // 时间窗口为10秒,每个窗口采样5个请求

        // 模拟请求流量
        for (int i = 0; i < 10; i++) {
            if (sampleWindow.allowRequest()) {
                System.out.println("处理请求 " + i);
                sampleWindow.addSample(); // 处理请求后添加新的请求样本
            } else {
                System.out.println("请求被限制 " + i);
            }
            try {
                Thread.sleep(2000); // 模拟请求间隔时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

4、令牌桶算法

描述: 令牌桶算法是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求;令牌桶限制的是平均流入速率,允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌;令牌桶允许一定程度的突发。

image.png

优点: 允许处理一定程度的突发流量,限制了平均流入速率。

缺点: 需要处理令牌的生成和消耗,实现较为复杂。

适用场景:  需要精确控制请求处理速率,并能够处理一定程度的突发请求。

Java语言实现参考:

import java.util.concurrent.*;

public class TokenBucketRateLimiter {
    private final int capacity; // 令牌桶的容量
    private final double rate; // 令牌生成速率,单位:令牌/毫秒
    private double tokens; // 当前令牌数量
    private long lastRefillTimestamp; // 上次令牌生成的时间戳

    public TokenBucketRateLimiter(int capacity, double rate) {
        this.capacity = capacity;
        this.rate = rate;
        this.tokens = 0;
        this.lastRefillTimestamp = System.currentTimeMillis();
    }

    public synchronized boolean allowRequest(int tokensRequested) {
        refillTokens();
        if (tokens >= tokensRequested) {
            tokens -= tokensRequested;
            return true;
        }
        return false;
    }

    private void refillTokens() {
        long now = System.currentTimeMillis();
        double timeSinceLastRefill = now - lastRefillTimestamp;
        tokens = Math.min(capacity, tokens + timeSinceLastRefill * rate);
        lastRefillTimestamp = now;
    }

    public static void main(String[] args) {
        TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(10, 0.01); // 每毫秒生成0.01个令牌,令牌桶容量为10
        for (int i = 0; i < 20; i++) {
            if (limiter.allowRequest(1)) {
                System.out.println("Allow request " + (i + 1));
            } else {
                System.out.println("Limit exceeded for request " + (i + 1));
            }
            try {
                Thread.sleep(100); // 模拟请求间隔
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

5、漏桶算法

描述: 漏桶算法则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝。漏桶限制的是常量流出速率,即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2,从而平滑突发流入速率;漏桶主要目的是平滑流出速率。

image.png

优点: 平滑请求流出速率,防止突发请求对系统造成影响。

缺点: 无法处理突发的流入请求,可能导致部分请求被拒绝。

适用场景:  需要稳定的输出流量,并且可以接受一定程度的请求丢弃或延迟。

Java语言实现参考:

import java.util.concurrent.*;

public class LeakyBucketRateLimiter {
    private final int capacity; // 漏桶的容量
    private final double rate; // 漏桶的漏出速率,单位:请求数/毫秒
    private double water; // 当前漏桶中的水量
    private long lastLeakTimestamp; // 上次漏水的时间戳

    public LeakyBucketRateLimiter(int capacity, double rate) {
        this.capacity = capacity;
        this.rate = rate;
        this.water = 0;
        this.lastLeakTimestamp = System.currentTimeMillis();
    }

    public synchronized boolean allowRequest() {
        leakWater();
        if (water < capacity) {
            water++;
            return true;
        }
        return false;
    }

    private void leakWater() {
        long now = System.currentTimeMillis();
        double timeSinceLastLeak = now - lastLeakTimestamp;
        water = Math.max(0, water - timeSinceLastLeak * rate);
        lastLeakTimestamp = now;
    }

    public static void main(String[] args) {
        LeakyBucketRateLimiter limiter = new LeakyBucketRateLimiter(10, 0.1); // 每毫秒漏出0.1个请求,漏桶容量为10
        for (int i = 0; i < 20; i++) {
            if (limiter.allowRequest()) {
                System.out.println("Allow request " + (i + 1));
            } else {
                System.out.println("Limit exceeded for request " + (i + 1));
            }
            try {
                Thread.sleep(100); // 模拟请求间隔
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

总结

算法本身没有优劣,需要根据实际应用场景和性能要求选择合适的算法。总体来说,固定窗口和滑动窗口适合简单的请求限流控制,令牌桶算法适合对请求速率有严格要求的场景,而样本窗口算法则更适合需要动态调整的复杂流量控制场景。