4. Rate Limiting 基础:令牌桶与漏桶算法原理及适用场景
在构建高并发系统时,Rate Limiting(限流)是保护后端服务的第一道防线。当连接池即将耗尽时,合理的限流策略可以防止系统被突发流量冲垮。本章深入剖析两种最经典的限流算法——令牌桶与漏桶,从原理、实现到适用场景进行系统性对比。
4.1 漏桶算法(Leaky Bucket)
漏桶算法的核心思想是将请求看作水流,系统是一个底部有固定大小漏洞的桶。无论进水速率如何,水(请求)从漏洞流出的速率是恒定的。当桶满时,新进入的水会溢出(请求被拒绝)。
4.2 令牌桶算法(Token Bucket)
令牌桶算法以固定速率向桶中放入令牌,每个请求需要消耗一个令牌才能被处理。与漏桶不同,令牌桶允许在桶中积累令牌,从而能够应对突发流量——只要桶中有足够的令牌,请求可以瞬间全部通过。
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):推荐令牌桶算法。缓存层通常能处理短时突发,令牌桶可以充分利用缓存性能。
避坑指南:不要在所有场景都使用同一种限流算法。在连接池耗尽压测中,如果使用漏桶算法保护数据库连接池,务必设置合理的桶容量——过小会导致大量请求被误杀,过大则失去保护意义。建议通过压测确定桶容量 = 连接池最大连接数 × 平均响应时间(秒)。