常用限流演算法

zhangleinewcharm發表於2024-05-28

限流(Rate Limiting)是保護系統穩定性、最佳化資源使用和提高服務質量的重要手段,通常在高併發環境下防止系統過載。

常見的限流演算法有令牌桶(Token Bucket)、漏桶(Leaky Bucket)、固定視窗計數器(Fixed Window Counter)、滑動視窗計數器(Sliding Window Counter)和滑動視窗日誌(Sliding Window Log)。以下是這些演算法的詳細原理、優缺點分析及其Java程式碼實現示例。

1. 令牌桶演算法(Token Bucket)

原理

  • 令牌以固定速率生成並存放在桶中。
  • 每個請求消耗一個令牌,桶中無令牌時請求被拒絕或延遲處理。
  • 桶的容量限制了令牌的最大數量,防止過多的令牌堆積。

優缺點

優點:

  • 靈活性高,支援突發流量。
  • 容易實現。

缺點:

  • 需要精確計時和鎖機制,可能增加系統開銷。

Java 實現示例

import java.util.concurrent.locks.ReentrantLock;

/**
 * 令牌桶演算法實現
 */
public class TokenBucket {
    private final long capacity; // 桶的最大容量
    private final long tokensPerSecond; // 每秒生成的令牌數
    private long tokens; // 當前令牌數
    private long lastRefillTimestamp; // 上次填充令牌的時間戳
    private final ReentrantLock lock = new ReentrantLock(); // 執行緒安全鎖

    /**
     * 建構函式
     * @param capacity 桶的最大容量
     * @param tokensPerSecond 每秒生成的令牌數
     */
    public TokenBucket(long capacity, long tokensPerSecond) {
        this.capacity = capacity;
        this.tokensPerSecond = tokensPerSecond;
        this.tokens = capacity; // 初始令牌數等於桶的容量
        this.lastRefillTimestamp = System.nanoTime(); // 初始化上次填充時間
    }

    /**
     * 是否允許請求
     * @return 如果允許請求返回true,否則返回false
     */
    public boolean allowRequest() {
        lock.lock();
        try {
            refill(); // 嘗試填充令牌
            if (tokens > 0) {
                tokens--; // 消耗一個令牌
                return true;
            }
            return false;
        } finally {
            lock.unlock();
        }
    }

    /**
     * 填充令牌
     */
    private void refill() {
        long now = System.nanoTime();// 獲取當前時間(納秒)
        long tokensToAdd = (now - lastRefillTimestamp) * tokensPerSecond / 1_000_000_000;// 計算從上一次填充到現在產生的令牌數
        tokens = Math.min(capacity, tokens + tokensToAdd);// 更新令牌數,但不超過桶的容量
        lastRefillTimestamp = now;// 更新上一次填充的時間戳
    }

    public static void main(String[] args) throws InterruptedException {
        TokenBucket bucket = new TokenBucket(10, 1); // 建立令牌桶,容量為10,每秒生成1個令牌
        for (int i = 0; i < 20; i++) {
            if (bucket.allowRequest()) {
                System.out.println("Request allowed");
            } else {
                System.out.println("Request denied");
            }
            Thread.sleep(100); // 每100毫秒發起一次請求
        }
    }
}

2. 漏桶演算法(Leaky Bucket)

原理

  • 請求以固定速率處理,超過容量的請求被丟棄。
  • 類似於一個裝滿水的桶,水以固定速率漏出。

優缺點

優點:

  • 控制流量的輸出速率,非常穩定。
  • 簡單易實現。

缺點:

  • 突發流量處理能力差。

Java 實現示例

import java.util.LinkedList;
import java.util.Queue;

/**
 * 漏桶演算法實現
 */
public class LeakyBucket {
    private final int capacity; // 桶的最大容量
    private final long leakRatePerSecond; // 每秒漏出的請求數
    private long lastLeakTimestamp; // 上次漏出的時間戳
    private final Queue<Long> requests = new LinkedList<>(); // 儲存請求的時間戳

    /**
     * 建構函式
     * @param capacity 桶的最大容量
     * @param leakRatePerSecond 每秒漏出的請求數
     */
    public LeakyBucket(int capacity, long leakRatePerSecond) {
        this.capacity = capacity;
        this.leakRatePerSecond = leakRatePerSecond;
        this.lastLeakTimestamp = System.nanoTime(); // 初始化上次漏出時間
    }

    /**
     * 是否允許請求
     * @return 如果允許請求返回true,否則返回false
     */
    public synchronized boolean allowRequest() {
        leak(); // 嘗試漏出請求
        if (requests.size() < capacity) {
            requests.add(System.nanoTime()); // 新增請求時間戳
            return true;
        }
        return false;
    }

    /**
     * 漏出請求
     */
    private void leak() {
        long now = System.nanoTime();// 獲取當前時間(納秒)
        long elapsed = now - lastLeakTimestamp;// 計算自上次漏水以來的時間差(納秒)
        long leaks = elapsed * leakRatePerSecond / 1_000_000_000;// 計算在時間差內應該漏出的請求數
        for (int i = 0; i < leaks && !requests.isEmpty(); i++) {
            requests.poll(); // 移除漏出的請求
        }
        lastLeakTimestamp = now;// 更新上一次漏水的時間戳
    }

    public static void main(String[] args) throws InterruptedException {
        LeakyBucket bucket = new LeakyBucket(10, 1); // 建立漏桶,容量為10,每秒漏出1個請求
        for (int i = 0; i < 20; i++) {
            if (bucket.allowRequest()) {
                System.out.println("Request allowed");
            } else {
                System.out.println("Request denied");
            }
            Thread.sleep(100); // 每100毫秒發起一次請求
        }
    }
}

3. 固定視窗計數器(Fixed Window Counter)

原理

  • 將時間劃分為固定長度的視窗,每個視窗內請求數有上限。

優缺點

優點:

  • 實現簡單。
  • 易於理解。

缺點:

  • 視窗邊界問題,可能會導致突發流量在視窗邊界時被接受。

Java 實現示例

import java.util.concurrent.locks.ReentrantLock;

/**
 * 固定視窗計數器實現
 */
public class FixedWindowCounter {
    private final int rate; // 每個視窗允許的最大請求數
    private final long windowSizeInMillis; // 視窗大小(毫秒)
    private long windowStart; // 當前視窗的起始時間
    private int requestCount; // 當前視窗內的請求數
    private final ReentrantLock lock = new ReentrantLock(); // 執行緒安全鎖

    /**
     * 建構函式
     * @param rate 每個視窗允許的最大請求數
     * @param windowSizeInMillis 視窗大小(毫秒)
     */
    public FixedWindowCounter(int rate, long windowSizeInMillis) {
        this.rate = rate;
        this.windowSizeInMillis = windowSizeInMillis;
        this.windowStart = System.currentTimeMillis();
        this.requestCount = 0;
    }

    /**
     * 是否允許請求
     * @return 如果允許請求返回true,否則返回false
     */
    public boolean allowRequest() {
        lock.lock();
        try {
            long now = System.currentTimeMillis();
            if (now >= windowStart + windowSizeInMillis) {
                windowStart = now;
                requestCount = 0; // 重置請求計數
            }
            if (requestCount < rate) {
                requestCount++;
                return true;
            }
            return false;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        FixedWindowCounter counter = new FixedWindowCounter(10, 1000); // 建立固定視窗計數器,每秒最多允許10個請求
        for (int i = 0; i < 20; i++) {
            if (counter.allowRequest()) {
                System.out.println("Request allowed");
            } else {
                System.out.println("Request denied");
            }
            Thread.sleep(100); // 每100毫秒發起一次請求
        }
    }
}

4. 滑動視窗計數器(Sliding Window Counter)

原理

  • 將時間視窗細分為多個小視窗,統計所有小視窗的請求數。

優缺點

優點:

  • 更精確地控制流量。
  • 可以更好地處理突發流量。

缺點:

  • 實現較為複雜。
  • 需要更多的儲存和計算資源。

Java 實現示例

import java.util.Deque;
import java.util.LinkedList;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 滑動視窗計數器實現
 */
public class SlidingWindowCounter {
    private final int rate; // 每個視窗允許的最大請求數
    private final long windowSizeInMillis; // 視窗大小(毫秒)
    private final Deque<Long> requestTimestamps = new LinkedList<>(); // 儲存請求的時間戳
    private final ReentrantLock lock = new ReentrantLock(); // 執行緒安全鎖

    /**
     * 建構函式
     * @param rate 每個視窗允許的最大請求數
     * @param windowSizeInMillis 視窗大小(毫秒)
     */
    public SlidingWindowCounter(int rate, long windowSizeInMillis) {
        this.rate = rate;
        this.windowSizeInMillis = windowSizeInMillis;
    }

    /**
     * 是否允許請求
     * @return 如果允許請求返回true,否則返回false
     */
    public boolean allowRequest() {
        long now = System.currentTimeMillis();
        lock.lock();
        try {
            while (!requestTimestamps.isEmpty() && requestTimestamps.peekFirst() <= now - windowSizeInMillis) {
                requestTimestamps.pollFirst(); // 移除不在視窗內的請求
            }
            if (requestTimestamps.size() < rate) {
                requestTimestamps.addLast(now); // 新增當前請求時間戳
                return true;
            }
            return false;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SlidingWindowCounter counter = new SlidingWindowCounter(10, 1000); // 建立滑動視窗計數器,每秒最多允許10個請求
        for (int i = 0; i < 20; i++) {
            if (counter.allowRequest()) {
                System.out.println("Request allowed");
            } else {
                System.out.println("Request denied");
            }
            Thread.sleep(100); // 每100毫秒發起一次請求
        }
    }
}

5. 滑動視窗日誌(Sliding Window Log)

原理

  • 記錄每個請求的時間戳,動態統計當前視窗內的請求數。

優缺點

優點:

  • 精確控制流量。
  • 更好地處理突發流量。

缺點:

  • 實現複雜。
  • 儲存和計算開銷較大。

Java 實現示例

import java.util.Deque;
import java.util.LinkedList;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 滑動視窗日誌實現
 */
public class SlidingWindowLog {
    private final int rate; // 每個視窗允許的最大請求數
    private final long windowSizeInMillis; // 視窗大小(毫秒)
    private final Deque<Long> requestTimestamps = new LinkedList<>(); // 儲存請求的時間戳
    private final ReentrantLock lock = new ReentrantLock(); // 執行緒安全鎖

    /**
     * 建構函式
     * @param rate 每個視窗允許的最大請求數
     * @param windowSizeInMillis 視窗大小(毫秒)
     */
    public SlidingWindowLog(int rate, long windowSizeInMillis) {
        this.rate = rate;
        this.windowSizeInMillis = windowSizeInMillis;
    }

    /**
     * 是否允許請求
     * @return 如果允許請求返回true,否則返回false
     */
    public boolean allowRequest() {
        long now = System.currentTimeMillis();
        lock.lock();
        try {
            while (!requestTimestamps.isEmpty() && requestTimestamps.peekFirst() <= now - windowSizeInMillis) {
                requestTimestamps.pollFirst(); // 移除不在視窗內的請求
            }
            if (requestTimestamps.size() < rate) {
                requestTimestamps.addLast(now); // 新增當前請求時間戳
                return true;
            }
            return false;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SlidingWindowLog log = new SlidingWindowLog(10, 1000); // 建立滑動視窗日誌,每秒最多允許10個請求
        for (int i = 0; i < 20; i++) {
            if (log.allowRequest()) {
                System.out.println("Request allowed");
            } else {
                System.out.println("Request denied");
            }
            Thread.sleep(100); // 每100毫秒發起一次請求
        }
    }
}

這些限流演算法各有優缺點,適用於不同的場景。選擇合適的限流演算法取決於具體的應用需求和系統特性。希望這些詳細的原理說明和Java程式碼實現示例能幫助你更好地理解和應用限流演算法。

相關文章