令牌桶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機,有錢就能取;漏桶像水龍頭,固定流速出水。

完活!