令牌桶VS漏桶:誰才是流量控制的「最適解」?
2025.08.27

面試被問到限流演算法,很多面試官會讓直接手寫令牌桶和漏桶的實現。雖然平常用過Redis、Guava等現成的限流工具,但我真要手寫還是有點慌張。今天就來聊聊這兩種經典限流演算法的差別,並用Java手寫實作。
很多的限流工具底層都應用了它們
一、令牌桶vs 漏桶:核心區別
令牌桶
令牌桶的核心思想:固定容量的桶,以固定速率往桶裡放令牌,請求來了就從桶拿令牌,沒令牌就拒絕。
有點像買票進站,想去坐火車就先去售票窗口買票,買到票了就憑票進入,買不到等待,因為窗口會定時的放票,再去搶。
下圖是用Ai生成的,大致可以體現出這麼意思
令牌桶特點:
1.可以處理突發流量(桶裡有令牌就能用),因為並不是一直請求都很多,但會一直以固定速率向桶裡添加令牌,請求少時桶內令牌滿了,請求激增可以滿桶拿令牌頂一陣
2、原理和實作上相對簡單
3.記憶體佔用小
漏桶適用場景:
- 介面限流:保護業務系統或敏感介面
- 防止惡意攻擊:抵禦Dos或DDos攻擊
- ……
它的優點在於能夠限制平均速率,同時允許一定的突發流量
漏桶
漏桶的核心思想比令牌桶早更簡單:請求像水一樣流入桶中,桶以固定速率「漏水」處理請求,超出桶容量的請求被丟棄或排隊。
漏桶的特性:
1.輸出非常平滑穩定
2.能有效保護下游系統(流量平滑)
3、❌ 無法處理突發流量
4、❌ 可能造成請求延遲
漏桶適用場景:
- 資料庫連線池:保護資料庫不被過載
- 訊息隊列消費:控制消費速率
- 支付系統:確保支付處理穩定性
二、手寫實現
令牌桶實現
public class TokenBucket {
// 桶容量(最大令牌数)
privatefinallong capacity;
// 令牌填充速率(令牌/秒)
privatefinallong refillRate;
// 当前令牌数量
private AtomicLong tokens;
// 上次填充时间戳(纳秒)
privatelong lastRefillTime;
public TokenBucket(long capacity, long refillRate) {
this.capacity = capacity;
this.refillRate = refillRate;
this.tokens = new AtomicLong(capacity);
this.lastRefillTime = System.nanoTime();
}
// 示例使用
public static void main(String[] args) throws InterruptedException {
// 创建桶:容量10令牌,每秒填充5令牌
TokenBucket bucket = new TokenBucket(10, 2);
// 模拟请求
for (int i = 1; i <= 50; i++) {
if (bucket.tryAcquire()) {
System.out.println("请求" + i + ": 通过");
} else {
System.out.println("请求" + i + ": 限流");
}
Thread.sleep(100); // 100ms请求一次
}
}
/**
* 尝试获取令牌
*
* @return true-获取成功,false-被限流
*/
public synchronized boolean tryAcquire() {
refillTokens();
if (tokens.get() > 0) {
tokens.decrementAndGet();
returntrue;
}
returnfalse;
}
/**
* 尝试获取多个令牌
*
* @param numTokens 请求令牌数
*/
public synchronized boolean tryAcquire(int numTokens) {
refillTokens();
if (tokens.get() >= numTokens) {
tokens.addAndGet(-numTokens);
returntrue;
}
returnfalse;
}
// 根据时间差补充令牌
private void refillTokens() {
long now = System.nanoTime();
// 计算时间差(秒)
double elapsedSec = (now - lastRefillTime) * 1e-9;
// 计算应补充的令牌数
long tokensToAdd = (long) (elapsedSec * refillRate);
if (tokensToAdd > 0) {
tokens.set(Math.min(capacity, tokens.get() + tokensToAdd));
lastRefillTime = now;
}
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 使用AtomicLong 保證線程安全。
- 透過時間差動態計算補充的令牌數。
- 桶容量限制突發流量的最大值。
圖片
漏桶實現
import java.util.concurrent.atomic.AtomicLong;
publicclass LeakyBucket {
// 桶容量(最大请求数)
privatefinallong capacity;
// 漏水速率(请求/秒)
privatefinallong leakRate;
// 当前水量(待处理请求数)
private AtomicLong water;
// 上次漏水时间戳(毫秒)
privatelong lastLeakTime;
public LeakyBucket(long capacity, long leakRate) {
this.capacity = capacity;
this.leakRate = leakRate;
this.water = new AtomicLong(0);
this.lastLeakTime = System.currentTimeMillis();
}
// 示例使用
public static void main(String[] args) throws InterruptedException {
// 创建桶:容量5请求,每秒处理2请求
LeakyBucket bucket = new LeakyBucket(5, 1);
// 模拟请求
for (int i = 1; i <= 15; i++) {
if (bucket.tryPass()) {
System.out.println("请求" + i + ": 通过 (当前水位: " + bucket.water.get() + ")");
} else {
System.out.println("请求" + i + ": 限流 (水位溢出)");
}
Thread.sleep(200); // 200ms请求一次
}
}
/**
* 尝试通过漏桶
*
* @return true-允许通过,false-被限流
*/
public synchronized boolean tryPass() {
leakWater();
if (water.get() < capacity) {
water.incrementAndGet();
returntrue;
}
returnfalse;
}
// 根据时间差漏水
private void leakWater() {
long now = System.currentTimeMillis();
// 计算时间差(秒)
long elapsedMs = now - lastLeakTime;
if (elapsedMs > 0) {
// 计算漏水量
long leaked = (long) (elapsedMs * leakRate / 1000.0);
if (leaked > 0) {
water.updateAndGet(cur -> Math.max(0, cur - leaked));
lastLeakTime = now;
}
}
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 漏出速率固定,確保請求處理平滑。
- 水量超過容量時直接拒絕請求。
圖片
三、測試對比
public class RateLimiterTest {
public static void main(String[] args) throws InterruptedException {
// 测试令牌桶:容量10,每秒填充5个令牌
TokenBucket tokenBucket = new TokenBucket(10, 5);
// 测试漏桶:容量10,每秒漏出5个请求
LeakyBucket leakyBucket = new LeakyBucket(10, 5);
System.out.println("=== 令牌桶测试(支持突发) ===");
testTokenBucket(tokenBucket);
Thread.sleep(1000);
System.out.println("\n=== 漏桶测试(平滑输出) ===");
testLeakyBucket(leakyBucket);
}
private static void testTokenBucket(TokenBucket bucket) {
// 模拟突发请求
for (int i = 0; i < 15; i++) {
boolean success = bucket.tryConsume(1);
System.out.printf("请求%d: %s (当前令牌: %.1f)%n",
i + 1, success ? "通过" : "拒绝", bucket.getCurrentTokens());
}
}
private static void testLeakyBucket(LeakyBucket bucket) {
// 模拟突发请求
for (int i = 0; i < 15; i++) {
boolean success = bucket.tryConsume();
System.out.printf("请求%d: %s (当前水量: %.1f)%n",
i + 1, success ? "通过" : "拒绝", bucket.getCurrentWater());
}
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
四、面試要點總結
面試官可能會問的問題:
Q: 兩種演算法的核心差異是什麼?
A: 令牌桶允許突發,漏桶強制平滑輸出
Q: 什麼場景用令牌桶,什麼場景用漏桶?
A: 需要處理突發用令牌桶,需要保護下游用漏桶
Q: 如何選擇桶的容量和速率?
A: 根據業務高峰、系統承載能力、使用者體驗綜合考慮
Q: 分散式環境下如何實現?
A: 可以用Redis實現,或是用一致性哈希分片
說在後邊
手寫限流演算法是一般在高層次的面試中不太會出現,但它們的基礎概念要掌握,在考場景題時它們都是不錯的方案。
簡單記:令牌桶像ATM機,有錢就能取;漏桶像水龍頭,固定流速出水。
完活!