找回密码
 立即注册
首页 业界区 安全 令牌桶VS漏桶:谁才是流量控制的“最优解”? ...

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

垢峒 前天 09:38
大家好,我是小富~
面试被问到限流算法,很多面试官会让直接手写令牌桶和漏桶的实现。虽然平时用过Redis、Guava等现成的限流工具,但真要手写还是有点慌。今天就来聊聊这两种经典限流算法的区别,并用Java手写实现。
很多的限流工具底层都应用了它们
一、令牌桶 vs 漏桶:核心区别

令牌桶

令牌桶的核心思想:固定容量的桶,以固定速率往桶里放令牌,请求来了就从桶拿令牌,没令牌就拒绝。
有点像买票进站,想去坐火车就先去售票窗口买票,买到票了就凭票进入,买不到等待,因为窗口会定时的放票,再去抢。
下图是用Ai生成的,大致能体现出这么个意思
1.png

令牌桶特点:
1、可以处理突发流量(桶里有令牌就能用),因为并不是一直请求都很多,但会一直以固定速率向桶里添加令牌,请求少时桶内令牌满了,请求激增可以满桶拿令牌顶一阵
2、原理和实现上相对简单
3、内存占用小
漏桶适用场景:

接口限流:保护业务系统或者敏感接口
防止恶意攻击:抵御Dos或DDos攻击
……
它的优势在于能够限制平均速率,同时允许一定的突发流量
漏桶

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

漏桶的特点:
1、输出非常平滑稳定
2、能有效保护下游系统(流量平滑)
3、❌ 无法处理突发流量
4、❌ 可能造成请求延迟
漏桶适用场景:

数据库连接池:保护数据库不被过载
消息队列消费:控制消费速率
支付系统:确保支付处理稳定性
二、手写实现

令牌桶实现
  1. public class TokenBucket {
  2.     // 桶容量(最大令牌数)
  3.     privatefinallong capacity;
  4.     // 令牌填充速率(令牌/秒)
  5.     privatefinallong refillRate;
  6.     // 当前令牌数量
  7.     private AtomicLong tokens;
  8.     // 上次填充时间戳(纳秒)
  9.     privatelong lastRefillTime;
  10.     public TokenBucket(long capacity, long refillRate) {
  11.         this.capacity = capacity;
  12.         this.refillRate = refillRate;
  13.         this.tokens = new AtomicLong(capacity);
  14.         this.lastRefillTime = System.nanoTime();
  15.     }
  16.     // 示例使用
  17.     public static void main(String[] args) throws InterruptedException {
  18.         // 创建桶:容量10令牌,每秒填充5令牌
  19.         TokenBucket bucket = new TokenBucket(10, 2);
  20.         // 模拟请求
  21.         for (int i = 1; i <= 50; i++) {
  22.             if (bucket.tryAcquire()) {
  23.                 System.out.println("请求" + i + ": 通过");
  24.             } else {
  25.                 System.out.println("请求" + i + ": 限流");
  26.             }
  27.             Thread.sleep(100); // 100ms请求一次
  28.         }
  29.     }
  30.     /**
  31.      * 尝试获取令牌
  32.      *
  33.      * @return true-获取成功,false-被限流
  34.      */
  35.     public synchronized boolean tryAcquire() {
  36.         refillTokens();
  37.         if (tokens.get() > 0) {
  38.             tokens.decrementAndGet();
  39.             returntrue;
  40.         }
  41.         returnfalse;
  42.     }
  43.     /**
  44.      * 尝试获取多个令牌
  45.      *
  46.      * @param numTokens 请求令牌数
  47.      */
  48.     public synchronized boolean tryAcquire(int numTokens) {
  49.         refillTokens();
  50.         if (tokens.get() >= numTokens) {
  51.             tokens.addAndGet(-numTokens);
  52.             returntrue;
  53.         }
  54.         returnfalse;
  55.     }
  56.     // 根据时间差补充令牌
  57.     private void refillTokens() {
  58.         long now = System.nanoTime();
  59.         // 计算时间差(秒)
  60.         double elapsedSec = (now - lastRefillTime) * 1e-9;
  61.         // 计算应补充的令牌数
  62.         long tokensToAdd = (long) (elapsedSec * refillRate);
  63.         if (tokensToAdd > 0) {
  64.             tokens.set(Math.min(capacity, tokens.get() + tokensToAdd));
  65.             lastRefillTime = now;
  66.         }
  67.     }
  68. }
复制代码

  • 使用 AtomicLong 保证线程安全。
  • 通过时间差动态计算补充的令牌数。
  • 桶容量限制突发流量的最大值。
3.png

漏桶实现
  1. import java.util.concurrent.atomic.AtomicLong;
  2. publicclass LeakyBucket {
  3.     // 桶容量(最大请求数)
  4.     privatefinallong capacity;
  5.     // 漏水速率(请求/秒)
  6.     privatefinallong leakRate;
  7.     // 当前水量(待处理请求数)
  8.     private AtomicLong water;
  9.     // 上次漏水时间戳(毫秒)
  10.     privatelong lastLeakTime;
  11.     public LeakyBucket(long capacity, long leakRate) {
  12.         this.capacity = capacity;
  13.         this.leakRate = leakRate;
  14.         this.water = new AtomicLong(0);
  15.         this.lastLeakTime = System.currentTimeMillis();
  16.     }
  17.     // 示例使用
  18.     public static void main(String[] args) throws InterruptedException {
  19.         // 创建桶:容量5请求,每秒处理2请求
  20.         LeakyBucket bucket = new LeakyBucket(5, 1);
  21.         // 模拟请求
  22.         for (int i = 1; i <= 15; i++) {
  23.             if (bucket.tryPass()) {
  24.                 System.out.println("请求" + i + ": 通过 (当前水位: " + bucket.water.get() + ")");
  25.             } else {
  26.                 System.out.println("请求" + i + ": 限流 (水位溢出)");
  27.             }
  28.             Thread.sleep(200); // 200ms请求一次
  29.         }
  30.     }
  31.     /**
  32.      * 尝试通过漏桶
  33.      *
  34.      * @return true-允许通过,false-被限流
  35.      */
  36.     public synchronized boolean tryPass() {
  37.         leakWater();
  38.         if (water.get() < capacity) {
  39.             water.incrementAndGet();
  40.             returntrue;
  41.         }
  42.         returnfalse;
  43.     }
  44.     // 根据时间差漏水
  45.     private void leakWater() {
  46.         long now = System.currentTimeMillis();
  47.         // 计算时间差(秒)
  48.         long elapsedMs = now - lastLeakTime;
  49.         if (elapsedMs > 0) {
  50.             // 计算漏水量
  51.             long leaked = (long) (elapsedMs * leakRate / 1000.0);
  52.             if (leaked > 0) {
  53.                 water.updateAndGet(cur -> Math.max(0, cur - leaked));
  54.                 lastLeakTime = now;
  55.             }
  56.         }
  57.     }
  58. }
复制代码

  • 漏出速率固定,确保请求处理平滑。
  • 水量超过容量时直接拒绝请求。
4.png

三、测试对比
  1. public class RateLimiterTest {
  2.     public static void main(String[] args) throws InterruptedException {
  3.         // 测试令牌桶:容量10,每秒填充5个令牌
  4.         TokenBucket tokenBucket = new TokenBucket(10, 5);
  5.         // 测试漏桶:容量10,每秒漏出5个请求
  6.         LeakyBucket leakyBucket = new LeakyBucket(10, 5);
  7.         System.out.println("=== 令牌桶测试(支持突发) ===");
  8.         testTokenBucket(tokenBucket);
  9.         Thread.sleep(1000);
  10.         System.out.println("\n=== 漏桶测试(平滑输出) ===");
  11.         testLeakyBucket(leakyBucket);
  12.     }
  13.     private static void testTokenBucket(TokenBucket bucket) {
  14.         // 模拟突发请求
  15.         for (int i = 0; i < 15; i++) {
  16.             boolean success = bucket.tryConsume(1);
  17.             System.out.printf("请求%d: %s (当前令牌: %.1f)%n",
  18.                 i + 1, success ? "通过" : "拒绝", bucket.getCurrentTokens());
  19.         }
  20.     }
  21.     private static void testLeakyBucket(LeakyBucket bucket) {
  22.         // 模拟突发请求
  23.         for (int i = 0; i < 15; i++) {
  24.             boolean success = bucket.tryConsume();
  25.             System.out.printf("请求%d: %s (当前水量: %.1f)%n",
  26.                 i + 1, success ? "通过" : "拒绝", bucket.getCurrentWater());
  27.         }
  28.     }
  29. }
复制代码
四、面试要点总结

面试官可能会问的问题:
Q: 两种算法的核心区别是什么?
A: 令牌桶允许突发,漏桶强制平滑输出
Q: 什么场景用令牌桶,什么场景用漏桶?
A: 需要处理突发用令牌桶,需要保护下游用漏桶
Q: 如何选择桶的容量和速率?
A: 根据业务峰值、系统承载能力、用户体验综合考虑
Q: 分布式环境下如何实现?
A: 可以用Redis实现,或者用一致性哈希分片
说在后边

手写限流算法是一般在高级别的面试中不太会出现,但它们的基础概念要掌握,在考场景题时它们都是不错的方案。
简单记:令牌桶像ATM机,有钱就能取;漏桶像水龙头,固定流速出水。
完活!

来源:豆瓜网用户自行投稿发布,如果侵权,请联系站长删除

相关推荐

您需要登录后才可以回帖 登录 | 立即注册