限流(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程式碼實現示例能幫助你更好地理解和應用限流演算法。