4. 漏桶算法实现:基于Python的漏桶实现、与令牌桶的对比、适用场景分析

流量控制里,漏桶算法和令牌桶算法是两大经典。很多人觉得它们差不多,其实差别挺大。今天咱们就聊聊漏桶,我还会拿它和令牌桶做个对比。

先说说我个人的理解。漏桶算法,说白了就是一个固定容量的桶,底下有个小洞,水以恒定速率流出去。不管上面倒水多猛,下面出水始终匀速。这个特性决定了它的核心价值——削峰填谷。

4.1 漏桶算法的核心原理

漏桶有三个关键参数:

  • 桶容量(capacity):最多能存多少请求。超过这个量,直接丢弃。
  • 漏水速率(leak_rate):每秒能处理多少个请求。这是恒定值。
  • 当前水量(water):桶里积压的请求数。

每次新请求进来,先看看桶满了没。满了就拒绝,没满就放进去。然后系统以固定速率从桶里取请求处理。嗯,这里要注意:漏水速率决定了系统的最大吞吐能力,桶容量决定了能容忍的突发程度。

核心公式:当前水量 = max(0, 上次水量 - (当前时间 - 上次时间) × 漏水速率)

如果当前水量 + 1 > 桶容量,则拒绝请求;否则放行,水量+1。

4.2 Python实现:一个生产级的漏桶

我曾经在某个API网关项目里写过漏桶,踩过不少坑。下面这个版本是我觉得比较实用的:

import time
import threading

class LeakyBucket:
    def __init__(self, capacity, leak_rate):
        self.capacity = capacity          # 桶容量
        self.leak_rate = leak_rate        # 漏水速率(请求/秒)
        self.water = 0                    # 当前水量
        self.last_time = time.time()      # 上次漏水时间
        self.lock = threading.Lock()      # 线程安全

    def _leak(self):
        """计算漏水,更新水量"""
        now = time.time()
        elapsed = now - self.last_time
        # 漏掉的水量 = 时间差 × 速率
        leaked = elapsed * self.leak_rate
        self.water = max(0, self.water - leaked)
        self.last_time = now

    def allow_request(self):
        """判断是否允许请求通过"""
        with self.lock:
            self._leak()
            if self.water < self.capacity:
                self.water += 1
                return True
            return False

# 使用示例
bucket = LeakyBucket(capacity=10, leak_rate=2)  # 桶容量10,每秒处理2个

for i in range(20):
    if bucket.allow_request():
        print(f"请求 {i}: 通过")
    else:
        print(f"请求 {i}: 被限流")
    time.sleep(0.1)

避坑指南:我曾经在实现时忘了加锁,结果高并发下水量计算全乱套了。多线程环境一定要用锁保护共享状态。另外,_leak()方法每次请求都调用,如果时间精度要求高,可以考虑用定时器异步漏水。

4.3 漏桶 vs 令牌桶:到底差在哪?

很多人问我:漏桶和令牌桶,到底选哪个?我一般会反问:你的业务允许突发吗?

先看个对比表格:

对比维度 漏桶算法 令牌桶算法
输出速率 严格恒定 允许突发到峰值
突发处理 不处理突发,超出即丢弃 允许一定程度的突发
实现复杂度 简单,只需维护水量和时间 稍复杂,需维护令牌数和时间
适用场景 对输出速率有严格要求的场景 允许短时突发的场景
内存开销 低,仅需几个变量 低,类似

说白了,漏桶是“硬限速”,令牌桶是“软限速”。漏桶把流量磨平了再输出,令牌桶允许你攒一波令牌然后爆发出去。

举个例子:你想想看,如果是一个数据库连接池,你希望每秒最多建立10个连接。用漏桶就很合适,因为数据库受不了突发连接。但如果是CDN缓存,用户突然刷一下页面,你希望他能快速拿到数据,这时候令牌桶就更友好。

4.4 适用场景分析

根据我的项目经验,漏桶算法最适合以下场景:

  1. 下游系统脆弱:比如老旧的数据库、单机服务,它们扛不住突发流量。漏桶能保证输出平稳。
  2. 网络带宽控制:我记得有个视频转码服务,输出带宽有限。用漏桶控制每秒发送的数据量,避免网络拥塞。
  3. 任务队列调度:消费者处理能力固定,用漏桶把生产者塞进来的任务平滑化。
  4. API限流:对外暴露的API,要求调用方不能超过某个固定QPS。漏桶是最直观的方案。

注意:漏桶不适合需要应对突发流量的场景。比如电商秒杀、热点新闻推送,用户瞬间涌入,你希望尽快响应,而不是把请求都卡在桶里排队。这时候令牌桶更合适。

4.5 漏桶算法的变体

实际项目中,我见过几种漏桶的变体:

  • 带优先级的漏桶:桶里分多个队列,高优先级请求可以插队。我曾在日志系统中用过,错误日志优先处理。
  • 动态速率漏桶:漏水速率根据系统负载动态调整。比如CPU空闲时加快处理,繁忙时减慢。
  • 分布式漏桶:用Redis实现全局漏桶,多个服务实例共享一个桶。这个在微服务架构里很常见。

下面这张图展示了漏桶算法的完整流程:

漏桶算法核心流程 请求流入 桶是否已满? 丢弃请求 请求入桶(水量+1) 桶内排队等待 恒定速率处理请求 关键说明 • 桶容量:最大排队请求数 • 漏水速率:每秒处理请求数 • 超出容量直接丢弃,不等待

4.6 选型建议

最后给点实在的建议。如果你还在纠结用漏桶还是令牌桶,可以问自己三个问题:

  • 下游能扛突发吗? 不能 → 漏桶;能 → 令牌桶
  • 业务允许请求排队吗? 允许 → 漏桶;不允许 → 令牌桶(直接拒绝)
  • 需要平滑输出吗? 需要 → 漏桶;不需要 → 令牌桶

我个人习惯是:保护下游用漏桶,保护上游用令牌桶。这个原则在我做过的十几个限流项目里,基本没出过问题。

小技巧:如果实在拿不准,可以两个都实现,然后压测对比。我一般会在代码里做个策略模式,运行时动态切换。这样上线后还能根据实际表现调整。