限流算法实现

205 阅读4分钟

限流算法用于控制系统的访问速率,以保护系统免受过载或恶意攻击的影响。以下是一些常见的限流算法:

  1. 固定窗口计数器(Fixed Window Counter):将请求按固定时间窗口进行计数,当请求超过阈值时进行限流。但存在突发性请求时容易发生流量突刺的问题。

  2. 滑动窗口计数器(Sliding Window Counter):与固定窗口计数器类似,但是滑动窗口计数器允许时间窗口滑动,更加平滑地处理突发性请求。

  3. 令牌桶算法(Token Bucket):在固定时间间隔内产生令牌,每个请求需要消耗一个令牌才能被处理,当令牌桶中的令牌不足时,拒绝请求或延迟处理。适用于平滑限制请求速率。

  4. 漏桶算法(Leaky Bucket):请求以固定速率进入漏桶,当漏桶溢出时,拒绝请求或延迟处理。适用于平滑突发请求。

  5. 计数器窗口算法(Counting Window):将请求按时间窗口进行计数,并且将计数结果存储在时间窗口中,通过控制时间窗口的大小和数量来限制请求速率。

  6. 令牌桶 + 漏桶组合算法:综合利用令牌桶和漏桶算法的优点,适用于灵活控制请求速率和处理延迟。

以上是一些常见的限流算法,每种算法都有其优缺点和适用场景,选择合适的限流算法取决于系统的需求和性能要求。

以下是用 Java 实现的几种限流算法的简单示例:

  1. 固定窗口计数器(Fixed Window Counter)
import java.util.concurrent.atomic.AtomicInteger;

public class FixedWindowCounter {
    private final int limit;
    private AtomicInteger counter;
    
    public FixedWindowCounter(int limit) {
        this.limit = limit;
        this.counter = new AtomicInteger(0);
    }
    
    public synchronized boolean allowRequest() {
        int count = counter.incrementAndGet();
        if (count > limit) {
            counter.decrementAndGet();
            return false;
        }
        return true;
    }
    
    public static void main(String[] args) {
        FixedWindowCounter counter = new FixedWindowCounter(10);
        for (int i = 0; i < 15; i++) {
            if (counter.allowRequest()) {
                System.out.println("Request " + (i + 1) + ": Allowed");
            } else {
                System.out.println("Request " + (i + 1) + ": Rejected");
            }
        }
    }
}
  1. 滑动窗口计数器(Sliding Window Counter)
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.atomic.AtomicInteger;

public class SlidingWindowCounter {
    private final int windowSize;
    private final int limit;
    private final long windowDuration;
    private Deque<Long> window;
    private AtomicInteger counter;

    public SlidingWindowCounter(int windowSize, int limit, long windowDuration) {
        this.windowSize = windowSize;
        this.limit = limit;
        this.windowDuration = windowDuration;
        this.window = new ArrayDeque<>();
        this.counter = new AtomicInteger(0);
    }

    public synchronized boolean allowRequest() {
        long currentTime = System.currentTimeMillis();
        cleanExpiredWindows(currentTime);
        if (counter.get() < limit) {
            counter.incrementAndGet();
            window.addLast(currentTime);
            return true;
        }
        return false;
    }

    private void cleanExpiredWindows(long currentTime) {
        long windowStart = currentTime - windowDuration;
        while (!window.isEmpty() && window.peekFirst() < windowStart) {
            window.pollFirst();
            counter.decrementAndGet();
        }
        while (window.size() >= windowSize) {
            window.pollFirst();
            counter.decrementAndGet();
        }
    }

    public static void main(String[] args) {
        SlidingWindowCounter slidingWindowCounter = new SlidingWindowCounter(5, 3, 1000);
        for (int i = 0; i < 10; i++) {
            if (slidingWindowCounter.allowRequest()) {
                System.out.println("Request " + (i + 1) + ": Allowed");
            } else {
                System.out.println("Request " + (i + 1) + ": Rejected");
            }
            try {
                Thread.sleep(200); // 模拟请求间隔
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

滑动窗口计时器算法通过维护一个固定大小的窗口,窗口中记录了过去一段时间内的请求情况。当新的请求到来时,先清理过期的窗口,然后判断当前窗口中的请求数是否超过限制。

  1. 令牌桶算法(Token Bucket)
import java.util.concurrent.atomic.AtomicLong;

public class TokenBucket {
    private final long capacity;
    private final long rate;
    private AtomicLong tokens;
    private long lastRefillTime;

    public TokenBucket(long capacity, long rate) {
        this.capacity = capacity;
        this.rate = rate;
        this.tokens = new AtomicLong(capacity);
        this.lastRefillTime = System.currentTimeMillis();
    }

    public synchronized boolean allowRequest(int tokensRequested) {
        refill();
        long currentTokens = tokens.get();
        if (currentTokens >= tokensRequested) {
            tokens.addAndGet(-tokensRequested);
            return true;
        }
        return false;
    }

    private void refill() {
        long currentTime = System.currentTimeMillis();
        long elapsedTime = currentTime - lastRefillTime;
        long newTokens = elapsedTime * rate / 1000;
        if (newTokens > 0) {
            tokens.set(Math.min(capacity, tokens.get() + newTokens));
            lastRefillTime = currentTime;
        }
    }

    public static void main(String[] args) {
        TokenBucket tokenBucket = new TokenBucket(100, 10);
        for (int i = 0; i < 15; i++) {
            int tokensRequested = (int) (Math.random() * 20 + 1);
            if (tokenBucket.allowRequest(tokensRequested)) {
                System.out.println("Request " + (i + 1) + ": Allowed (" + tokensRequested + " tokens)");
            } else {
                System.out.println("Request " + (i + 1) + ": Rejected (" + tokensRequested + " tokens)");
            }
        }
    }
}
  1. 漏桶算法(Leaky Bucket)
public class LeakyBucket {
    private final long capacity;
    private final long rate;
    private long content;
    private long lastLeakTime;

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

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

    private void leak() {
        long currentTime = System.currentTimeMillis();
        long elapsedTime = currentTime - lastLeakTime;
        long leaked = elapsedTime * rate / 1000;
        if (leaked > 0) {
            content = Math.max(0, content - leaked);
            lastLeakTime = currentTime;
        }
    }

    public static void main(String[] args) {
        LeakyBucket leakyBucket = new LeakyBucket(10, 2);
        for (int i = 0; i < 15; i++) {
            if (leakyBucket.allowRequest()) {
                System.out.println("Request " + (i + 1) + ": Allowed");
            } else {
                System.out.println("Request " + (i + 1) + ": Rejected");
            }
        }
    }
}
  1. 计数器窗口算法(Counting Window)
import java.util.concurrent.ConcurrentHashMap;

public class CountingWindow {
    private final long windowSize;
    private final int limit;
    private ConcurrentHashMap<Long, Integer> window;

    public CountingWindow(long windowSize, int limit) {
        this.windowSize = windowSize;
        this.limit = limit;
        this.window = new ConcurrentHashMap<>();
    }

    public synchronized boolean allowRequest() {
        long currentTime = System.currentTimeMillis();
        long windowStart = currentTime - windowSize;
        // 清理过期的窗口
        for (long timestamp : window.keySet()) {
            if (timestamp < windowStart) {
                window.remove(timestamp);
            }
        }
        // 判断请求是否超过限制
        if (window.size() < limit) {
            window.put(currentTime, window.size() + 1);
            return true;
        }
        return false;
    }

    public static void main(String[] args) {
        CountingWindow countingWindow = new CountingWindow(1000, 5);
        for (int i = 0; i < 10; i++) {
            if (countingWindow.allowRequest()) {
                System.out.println("Request " + (i + 1) + ": Allowed");
            } else {
                System.out.println("Request " + (i + 1) + ": Rejected");
            }
            try {
                Thread.sleep(200); // 模拟请求间隔
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

5.令牌桶 + 漏桶组合算法

import java.util.concurrent.atomic.AtomicLong;

public class TokenBucketLeakyBucket {
    private final long capacity;
    private final long rate;
    private AtomicLong tokens;
    private long lastRefillTime;
    private final long leakRate;
    private long content;
    private long lastLeakTime;

    public TokenBucketLeakyBucket(long capacity, long rate, long leakRate) {
        this.capacity = capacity;
        this.rate = rate;
        this.tokens = new AtomicLong(capacity);
        this.lastRefillTime = System.currentTimeMillis();
        this.leakRate = leakRate;
        this.content = 0;
        this.lastLeakTime = System.currentTimeMillis();
    }

    public synchronized boolean allowRequest() {
        refill();
        leak();
        if (content < capacity && tokens.get() > 0) {
            tokens.decrementAndGet();
            content++;
            return true;
        }
        return false;
    }

    private void refill() {
        long currentTime = System.currentTimeMillis();
        long elapsedTime = currentTime - lastRefillTime;
        long newTokens = elapsedTime * rate / 1000;
        if (newTokens > 0) {
            tokens.set(Math.min(capacity, tokens.get() + newTokens));
            lastRefillTime = currentTime;
        }
    }

    private void leak() {
        long currentTime = System.currentTimeMillis();
        long elapsedTime = currentTime - lastLeakTime;
        long leaked = elapsedTime * leakRate / 1000;
        if (leaked > 0) {
            content = Math.max(0, content - leaked);
            lastLeakTime = currentTime;
        }
    }

    public static void main(String[] args) {
        TokenBucketLeakyBucket tokenBucketLeakyBucket = new TokenBucketLeakyBucket(10, 5, 2);
        for (int i = 0; i < 15; i++) {
            if (tokenBucketLeakyBucket.allowRequest()) {
                System.out.println("Request " + (i + 1) + ": Allowed");
            } else {
                System.out.println("Request " + (i + 1) + ": Rejected");
            }
        }
    }
}

这个算法同时使用了令牌桶和漏桶的思想。令牌桶部分控制了请求的速率,漏桶部分控制了请求的突发量,两者结合起来可以更灵活地控制流量。