令牌桶VS漏桶:谁才是流量控制的“最优解”?

大家好,我是小富~

面试被问到限流算法,很多面试官会让直接手写令牌桶和漏桶的实现。虽然平时用过Redis、Guava等现成的限流工具,但真要手写还是有点慌。今天就来聊聊这两种经典限流算法的区别,并用Java手写实现。

很多的限流工具底层都应用了它们

一、令牌桶 vs 漏桶:核心区别

令牌桶

令牌桶的核心思想:固定容量的桶,以固定速率往桶里放令牌,请求来了就从桶拿令牌,没令牌就拒绝。

有点像买票进站,想去坐火车就先去售票窗口买票,买到票了就凭票进入,买不到等待,因为窗口会定时的放票,再去抢。

下图是用Ai生成的,大致能体现出这么个意思

令牌桶特点:

1、可以处理突发流量(桶里有令牌就能用),因为并不是一直请求都很多,但会一直以固定速率向桶里添加令牌,请求少时桶内令牌满了,请求激增可以满桶拿令牌顶一阵

2、原理和实现上相对简单

3、内存占用小

漏桶适用场景:

接口限流:保护业务系统或者敏感接口

防止恶意攻击:抵御Dos或DDos攻击

...... 它的优势在于能够限制平均速率,同时允许一定的突发流量

漏桶

漏桶的核心思想比令牌桶早更简单:请求像水一样流入桶中,桶以固定速率"漏水"处理请求,超出桶容量的请求被丢弃或排队。

漏桶的特点:

1、输出非常平滑稳定

2、能有效保护下游系统(流量平滑)

3、❌ 无法处理突发流量

4、❌ 可能造成请求延迟

漏桶适用场景:

数据库连接池:保护数据库不被过载

消息队列消费:控制消费速率

支付系统:确保支付处理稳定性

二、手写实现

令牌桶实现

java 复制代码
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;
        }
    }
}
  • 使用 AtomicLong 保证线程安全。

  • 通过时间差动态计算补充的令牌数。

  • 桶容量限制突发流量的最大值。

漏桶实现

java 复制代码
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;
            }
        }
    }
}
  • 漏出速率固定,确保请求处理平滑。

  • 水量超过容量时直接拒绝请求。

三、测试对比

java 复制代码
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());
        }
    }
}

四、面试要点总结

面试官可能会问的问题:

Q: 两种算法的核心区别是什么?

A: 令牌桶允许突发,漏桶强制平滑输出

Q: 什么场景用令牌桶,什么场景用漏桶?

A: 需要处理突发用令牌桶,需要保护下游用漏桶

Q: 如何选择桶的容量和速率?

A: 根据业务峰值、系统承载能力、用户体验综合考虑

Q: 分布式环境下如何实现?

A: 可以用Redis实现,或者用一致性哈希分片

说在后边

手写限流算法是一般在高级别的面试中不太会出现,但它们的基础概念要掌握,在考场景题时它们都是不错的方案。

简单记:令牌桶像ATM机,有钱就能取;漏桶像水龙头,固定流速出水。

完活!

相关推荐
無限進步D3 小时前
Java 运行原理
java·开发语言·入门
難釋懷3 小时前
安装Canal
java
是苏浙3 小时前
JDK17新增特性
java·开发语言
不光头强3 小时前
spring cloud知识总结
后端·spring·spring cloud
GetcharZp6 小时前
告别 Python 依赖!用 LangChainGo 打造高性能大模型应用,Go 程序员必看!
后端
阿里加多7 小时前
第 4 章:Go 线程模型——GMP 深度解析
java·开发语言·后端·golang
likerhood7 小时前
java中`==`和`.equals()`区别
java·开发语言·python
小小李程序员7 小时前
Langchain4j工具调用获取不到ThreadLocal
java·后端·ai
IronMurphy7 小时前
【算法三十九】994. 腐烂的橘子
算法