目录
- 一、什么是漏桶算法?
- 二、动手实现一个简单的漏桶限流器
- 三、漏桶的优缺点
- 四、Q&A:个人思考
- Q1:严格控制每个请求的执行间隔真的有必要吗?漏桶还有使用场景吗?
- Q2:令牌桶和漏桶到底有什么区别?
- Q3:既然漏桶本质是“队列 + 定时任务”,那能不能直接用定时任务替代?
- Q4:如果限流超过 1000 QPS 怎么办?
- Q5:“为什么令牌桶允许突发,漏桶不允许?”
在应对突发流量时,限流是我们保障系统稳定性的第一道防线。常用的限流工具如Google Guava RateLimiter、Alibaba Sentinel、Redis + Lua 脚本(分布式限流)、Nginx 限流、Spring Cloud Gateway等,虽然功能各异,但底层大多基于四种经典算法:计数器(Counter)、滑动窗口(Sliding Window)、漏桶(LeakyBucket)和令牌桶(Token Bucket)。深入理解这些算法的原理,有助于我们更合理地选择和使用限流工具。
上一篇文章我们聊了令牌桶,今天来聊聊它的“孪生兄弟”——漏桶算法(Leaky Bucket)。本文将从以下几个方面展开:
- 什么是漏桶算法?
- 手动实现一个简单的漏桶
- 漏桶的优缺点分析
- 常见问题与作者思考(Q&A)
一、什么是漏桶算法?
漏桶算法(Leaky Bucket Algorithm)是一种经典的流量整形(Traffic Shaping)和限流(Rate Limiting)算法。它的核心思想非常直观:
无论输入流量多么突发或不均匀,输出流量始终保持恒定速率。
就像一个底部有小孔的水桶:
- 水可以快速倒入(请求可以瞬间涌入);
- 但水只能以固定速度从底部小孔漏出(系统以固定速率处理请求);
- 桶有固定容量,若水倒得太快导致溢出,则多余水流直接丢弃(请求被拒绝)。
这种机制天然具备平滑突发流量、保护下游系统的能力。
二、动手实现一个简单的漏桶限流器
在后端开发中,我们通常用一个有界队列来模拟漏桶:
- 桶容量= 队列的最大长度;
- 漏水速率= 后台定时任务以固定间隔从队列头部取出并“处理”请求。
伪代码大概长这样:
publicclassLeakyBucketLimiter{//用队列模拟桶(注意:实际要存的是要执行的任务,作者用Boolean代替了)privatefinalArrayBlockingQueue<Boolean>queue;//定时器privatefinalScheduledExecutorServicescheduler;//构造函数publicLeakyBucketLimiter(intcapacity,intrate){//参数校验if(capacity<=0||rate<=0){thrownewIllegalArgumentException("参数需要大于0");}//初始化桶this.queue=newArrayBlockingQueue<Boolean>(capacity);//计算漏水间隔,确保每个请求漏出的时间间隔相同longinterval=1000L/rate;//创建一个单线程调度器this.scheduler=Executors.newSingleThreadScheduledExecutor(r->{Threadt=newThread("LeakyBucketLimiter-Thread");//线程设置为守护线程,确保JVM退出时,线程也能退出t.setDaemon(true);returnt;});scheduler.scheduleAtFixedRate(this::leak,//要执行的任务interval,//初始延迟interval,//间隔时间TimeUnit.MILLISECONDS);}//提交请求publicbooleantryAcquire(){//实际要把任务加入队列,这里只是模拟returnqueue.offer(true);}privatevoidleak(){//从队头移除一个元素,模拟漏水queue.poll();}//关闭定时器,并清空队列publicvoidshutdown(){scheduler.shutdown();queue.clear();}}关于守护线程:守护线程(如 GC、JIT 编译线程)为用户线程服务。当所有非守护线程结束时,JVM 会直接退出,不再等待守护线程完成。
使用示例:
// 每秒最多 3 个请求,桶容量 = 3SimpleLeakyBucketlimiter=newSimpleLeakyBucket(3,3);for(inti=0;i<8;i++){if(limiter.tryAcquire()){//实际要把任务放进队列!!!System.out.println(" 请求 "+i+" 被接受");}else{System.out.println(" 请求 "+i+" 被拒绝(桶满)");}Thread.sleep(100);// 每 100ms 发一个请求(10 QPS > 3 QPS)}是不是很简单?核心逻辑就是:请求先入队,后台线程匀速消费。
三、漏桶的优缺点
优点:请求稳定,能很好利用性能,保护下游系统。
缺点:无法应对突发流量,容易积压请求,大量请求被拒绝。
正因为这些限制,纯漏桶在现代 Web 服务中较少单独使用,更多被令牌桶或滑动窗口取代。但在某些特定场景(如硬件对接、音视频帧率控制、流量整形网关),漏桶仍是不可替代的选择。
四、Q&A:个人思考
Q1:严格控制每个请求的执行间隔真的有必要吗?漏桶还有使用场景吗?
我觉得没必要,只有定时执行的场景(音/视频控制帧率)能用到,绝大部分都用令牌桶和滑动窗口。(你们怎么看?)
Q2:令牌桶和漏桶到底有什么区别?
- 漏桶:把请求放到缓冲队列,让它按时执行,队满拒绝。
- 令牌桶:按时往桶里发放令牌,到桶里拿令牌,能拿到令牌就能执行,否则拒绝。
Q3:既然漏桶本质是“队列 + 定时任务”,那能不能直接用定时任务替代?
漏桶 =有界队列 + 定时消费 + 入口拒绝。如果只用定时任务处理无界队列,会导致请求堆积、内存爆炸、延迟飙升——这已经不是限流,而是“慢消费”。真正的限流必须在入口快速拒绝超量请求。
Q4:如果限流超过 1000 QPS 怎么办?
当
rate > 1000时,interval = 1000 / rate可能小于 1ms,超出操作系统调度精度。此时应放弃高频定时器,改用懒更新 + 纳秒时间戳的方式(类似令牌桶的实现),在每次请求时动态计算可释放的配额。
Q5:“为什么令牌桶允许突发,漏桶不允许?”
令牌桶可以预存令牌,空闲时攒下的令牌可用于突发;
漏桶的队列只能排队等待,即使空闲,新请求也必须等系统按节奏“漏水”才能腾出空间。
但注意:漏桶允许“有限突发”——只要不超过桶容量!
🐾 欢迎留言讨论!作者爱思考,也爱和你一起探索技术本质~