4. Rate Limiting 基础:令牌桶与漏桶算法原理及适用场景

在构建高并发系统时,Rate Limiting(限流)是保护后端服务的第一道防线。当连接池即将耗尽时,合理的限流策略可以防止系统被突发流量冲垮。本章深入剖析两种最经典的限流算法——令牌桶与漏桶,从原理、实现到适用场景进行系统性对比。

4.1 漏桶算法(Leaky Bucket)

漏桶算法的核心思想是将请求看作水流,系统是一个底部有固定大小漏洞的桶。无论进水速率如何,水(请求)从漏洞流出的速率是恒定的。当桶满时,新进入的水会溢出(请求被拒绝)。

漏桶算法原理图 突发请求流入 桶容量上限 漏洞 恒定速率流出 溢出(拒绝) 核心特征 • 输出速率绝对恒定,不受输入影响 • 桶容量限制最大突发量 • 超出容量则直接丢弃请求 适用场景 • 数据库连接池保护(恒定QPS) • 日志写入、消息队列生产端 • 需要严格平滑流量的场景 缺点 • 无法应对突发流量,即使系统空闲

4.2 令牌桶算法(Token Bucket)

令牌桶算法以固定速率向桶中放入令牌,每个请求需要消耗一个令牌才能被处理。与漏桶不同,令牌桶允许在桶中积累令牌,从而能够应对突发流量——只要桶中有足够的令牌,请求可以瞬间全部通过。

令牌桶算法原理图 令牌生成器 固定速率放入令牌 T 令牌桶 T T T T T 桶容量上限 请求队列 消耗令牌 请求通过 请求拒绝 核心特征 • 允许一定程度的突发流量 • 长期平均速率受限于令牌生成速率 • 桶容量决定最大突发量 • 适合需要应对短时峰值的场景

4.3 算法对比与选型决策

对比维度 漏桶算法 令牌桶算法
输出速率 严格恒定 平均恒定,允许突发
突发处理 不支持突发 支持突发(受桶容量限制)
实现复杂度 低 中
内存占用 低(仅需计数器) 低(需计数器+时间戳)
流量整形效果 强(完全平滑) 弱(允许短时波动)
典型应用 数据库连接池、日志输出 API网关、微服务限流

4.4 代码实现示例

以下是一个基于令牌桶算法的限流器核心实现,使用 Go 语言编写,适用于高并发场景:

package limiter

import (
    "sync"
    "time"
)

type TokenBucket struct {
    rate       float64    // 令牌生成速率(个/秒)
    capacity   float64    // 桶容量
    tokens     float64    // 当前令牌数
    lastTime   time.Time  // 上次更新时间
    mu         sync.Mutex
}

func NewTokenBucket(rate, capacity float64) *TokenBucket {
    return &TokenBucket{
        rate:     rate,
        capacity: capacity,
        tokens:   capacity, // 初始满桶
        lastTime: time.Now(),
    }
}

// Allow 判断是否允许请求通过
func (tb *TokenBucket) Allow() bool {
    tb.mu.Lock()
    defer tb.mu.Unlock()

    now := time.Now()
    // 计算时间差,补充令牌
    elapsed := now.Sub(tb.lastTime).Seconds()
    tb.tokens += elapsed * tb.rate
    if tb.tokens > tb.capacity {
        tb.tokens = tb.capacity
    }
    tb.lastTime = now

    // 消耗令牌
    if tb.tokens >= 1 {
        tb.tokens--
        return true
    }
    return false
}

// AllowN 判断是否允许 N 个请求通过
func (tb *TokenBucket) AllowN(n float64) bool {
    tb.mu.Lock()
    defer tb.mu.Unlock()

    now := time.Now()
    elapsed := now.Sub(tb.lastTime).Seconds()
    tb.tokens += elapsed * tb.rate
    if tb.tokens > tb.capacity {
        tb.tokens = tb.capacity
    }
    tb.lastTime = now

    if tb.tokens >= n {
        tb.tokens -= n
        return true
    }
    return false
}
压测实践提示:在连接池耗尽场景下,推荐使用令牌桶算法作为前置限流。将桶容量设置为连接池最大连接数的 1.2~1.5 倍,令牌生成速率设置为连接池的稳态处理能力(QPS)。这样既能应对短时突发,又能防止连接池被持续打满。

4.5 连接池耗尽场景下的选型建议

  • 数据库连接池保护:推荐漏桶算法。数据库连接是稀缺资源,需要严格限制并发数,平滑流量可以避免连接池频繁创建/销毁。
  • 微服务 API 网关:推荐令牌桶算法。允许客户端在空闲后发起突发请求,提升用户体验,同时保证长期平均速率可控。
  • 消息队列生产端:推荐漏桶算法。消息中间件对写入速率敏感,恒定速率可以避免消息堆积和背压问题。
  • 缓存层保护(Redis/Memcached):推荐令牌桶算法。缓存层通常能处理短时突发,令牌桶可以充分利用缓存性能。
避坑指南:不要在所有场景都使用同一种限流算法。在连接池耗尽压测中,如果使用漏桶算法保护数据库连接池,务必设置合理的桶容量——过小会导致大量请求被误杀,过大则失去保护意义。建议通过压测确定桶容量 = 连接池最大连接数 × 平均响应时间(秒)。