4、漏桶实现:基于 time.Ticker 的漏桶实现、自定义漏桶结构体
漏桶算法(Leaky Bucket)是限流领域最直观的流量整形模型之一。与令牌桶的“积攒令牌再消费”不同,漏桶强制请求以恒定速率流出,超出桶容量的请求直接丢弃。在 Go 语言中,利用 time.Ticker 实现漏桶是最贴近底层系统时钟的工程方案。
4.1 漏桶核心模型与结构体设计
自定义漏桶结构体需要封装三个核心属性:桶容量(burst)、恒定流出速率(rate)、以及用于驱动周期性流出的 time.Ticker。同时需要记录当前桶中的“水位”(pending),即等待处理的请求数。
| 字段 | 类型 | 说明 |
|---|---|---|
capacity |
int64 |
桶的最大容量,超出此值的请求被拒绝 |
rate |
time.Duration |
每次漏出(处理)一个请求的时间间隔 |
ticker |
*time.Ticker |
周期性触发漏出操作的定时器 |
pending |
int64 |
当前桶中等待处理的请求数(原子操作) |
mu |
sync.Mutex |
保护 pending 和 ticker 状态变更的互斥锁 |
设计要点: 使用
sync/atomic 操作 pending 字段可减少锁竞争;但启动/停止 ticker 时必须加锁,防止并发关闭导致 panic。
4.2 基于 time.Ticker 的漏桶实现
以下代码展示了完整的自定义漏桶结构体及其核心方法:NewLeakyBucket、Allow、StartDrain、Stop。
package leakybucket
import (
"sync"
"sync/atomic"
"time"
)
// LeakyBucket 漏桶限流器
type LeakyBucket struct {
capacity int64 // 桶容量
rate time.Duration // 漏出间隔
ticker *time.Ticker // 周期性漏出定时器
pending int64 // 当前等待数(原子操作)
mu sync.Mutex // 保护 ticker 启停
done chan struct{} // 通知 drain goroutine 退出
}
// NewLeakyBucket 创建一个漏桶限流器
// capacity: 桶容量;rate: 每次漏出的时间间隔
func NewLeakyBucket(capacity int64, rate time.Duration) *LeakyBucket {
lb := &LeakyBucket{
capacity: capacity,
rate: rate,
done: make(chan struct{}),
}
return lb
}
// StartDrain 启动后台漏出协程
func (lb *LeakyBucket) StartDrain() {
lb.mu.Lock()
defer lb.mu.Unlock()
if lb.ticker != nil {
return // 已经启动
}
lb.ticker = time.NewTicker(lb.rate)
go func() {
for {
select {
case <-lb.ticker.C:
// 每次 tick 漏出一个请求
for {
current := atomic.LoadInt64(&lb.pending)
if current == 0 {
break
}
if atomic.CompareAndSwapInt64(&lb.pending, current, current-1) {
break
}
}
case <-lb.done:
return
}
}
}()
}
// Allow 尝试向桶中放入一个请求
// 返回 true 表示放入成功(请求被接受),false 表示桶满(请求被丢弃)
func (lb *LeakyBucket) Allow() bool {
for {
current := atomic.LoadInt64(&lb.pending)
if current >= lb.capacity {
return false // 桶满,拒绝
}
if atomic.CompareAndSwapInt64(&lb.pending, current, current+1) {
return true // 放入成功
}
// CAS 失败,重试
}
}
// Stop 停止漏桶,关闭 ticker 和 drain 协程
func (lb *LeakyBucket) Stop() {
lb.mu.Lock()
defer lb.mu.Unlock()
if lb.ticker != nil {
lb.ticker.Stop()
lb.ticker = nil
close(lb.done)
}
}
实现技巧: 漏出操作使用
for + CAS 循环,确保在高并发下 pending 递减的原子性。后台 drain goroutine 通过 done channel 安全退出,避免 goroutine 泄漏。
4.3 漏桶工作流程(内联 SVG)
下图展示了漏桶从请求入桶到恒定速率漏出的完整生命周期:
4.4 使用示例与边界场景
以下代码演示了如何创建漏桶、启动 drain 协程,并在 HTTP 处理函数中使用:
func main() {
// 桶容量 10,每 100ms 漏出一个请求(即 10 QPS)
lb := leakybucket.NewLeakyBucket(10, 100*time.Millisecond)
lb.StartDrain()
defer lb.Stop()
http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
if !lb.Allow() {
http.Error(w, "429 Too Many Requests", http.StatusTooManyRequests)
return
}
w.Write([]byte("OK"))
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
边界场景注意:
- rate 过小: 若 rate < 1ms,ticker 可能无法精确触发,建议最小间隔 1ms。
- capacity 为 0: 所有请求都会被拒绝,需在业务层校验。
- 并发 Stop: 多次调用
Stop()会导致 close(done) panic,需用sync.Once保护。 - drain 协程泄漏: 忘记调用
Stop()会导致 goroutine 永久运行。
4.5 漏桶 vs 令牌桶:核心差异
| 对比维度 | 漏桶(Leaky Bucket) | 令牌桶(Token Bucket) |
|---|---|---|
| 流量特性 | 强制恒定速率输出 | 允许突发流量(积攒令牌) |
| 实现核心 | time.Ticker + pending 计数 | 定时填充 + 令牌计数 |
| 丢弃策略 | 桶满即丢(请求入桶时) | 令牌不足时等待或丢弃 |
| 适用场景 | 数据库连接池、消息队列消费 | API 网关、突发流量保护 |
| Go 标准库支持 | 无,需自定义 | golang.org/x/time/rate |
审计建议: 在需要严格限制流出速率(如第三方 API 调用频率控制)时优先选择漏桶;在需要容忍短时突发(如用户秒杀场景)时选择令牌桶。生产环境中建议对
pending 和 capacity 添加 Prometheus 指标监控,便于观察限流水位。