news 2026/5/30 21:10:03

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
令牌桶VS漏桶:谁才是流量控制的“最优解”?

面试被问到限流算法,很多面试官会让直接手写令牌桶和漏桶的实现。虽然平时用过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; } } }
  • 使用 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; } } } }
  • 漏出速率固定,确保请求处理平滑。

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

三、测试对比

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机,有钱就能取;漏桶像水龙头,固定流速出水。

完活!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/30 5:38:18

springboot 分布式验证码登录的通用方案

为了防止世界被破坏&#xff0c;为了守护世界的和平。。。说错了&#xff0c;重来~ 为了防止验证系统被暴力破解&#xff0c;很多系统都增加了验证码效验&#xff0c;比较常见的就是图片二维码&#xff0c;业内比较安全的是短信验证码&#xff0c;当然还有一些拼图验证码&…

作者头像 李华
网站建设 2026/5/29 2:39:51

Java毕设项目推荐-基于springboot的汽车租赁买卖管理系统的设计与实现租赁与买卖二手车交易【附源码+文档,调试定制服务】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/5/29 23:30:20

【课程设计/毕业设计】基于springboot的影院购票管理系统的设计与实现基于 SpringBoot 的电影院购票系统设计与实现【附源码、数据库、万字文档】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/5/30 3:23:25

如何正确删除电脑的缓存文件?

新的电脑总是好的&#xff0c;各种干净整洁无垃圾。 还是新的好 表情包 使用了一段时间之后&#xff0c;小伙伴们就会发现电脑C盘飙红了。然后就各种论坛查找清除电脑垃圾的方法。 电脑正常使用下&#xff0c;是会产生很多缓存的&#xff0c;所以C盘红了也很正常。除非电脑组…

作者头像 李华
网站建设 2026/5/30 2:21:14

[python] 代码性能分析工具line_profiler使用指北

码分析能够评估各部分代码的时间消耗&#xff0c;即进行时间复杂度分析。通过这一过程&#xff0c;我们可以识别影响整体运行效率的关键部分&#xff0c;从而更高效地利用底层计算资源。此外&#xff0c;代码分析也可用于评估内存使用情况&#xff0c;即空间复杂度&#xff0c;…

作者头像 李华
网站建设 2026/5/30 18:54:00

《手搓》线程池

一、什么是《手搓》线程池手搓线程池并不是用来完全代替系统线程池的你可以把手搓线程池看做系统线程池的一部分就好比在东海用集装箱搞养殖一个集装箱里养鱼另一个集装箱里养虾搞好隔离,鱼虾都不耽搁二、最常用线程池的场景是什么当然是Task,是用TaskFactory.StartNew方法创建…

作者头像 李华