2. 限流核心概念:QPS、TPS、并发数、令牌桶、漏桶、计数器算法的原理与对比
在构建高可用 Go 业务接口时,限流是抵御流量洪峰的第一道防线。理解限流背后的核心度量指标与算法原理,是正确选型与落地的前提。本节将系统梳理 QPS、TPS、并发数三个关键指标,并深入剖析令牌桶、漏桶、计数器三种经典算法的机制与适用场景。
2.1 核心度量指标
限流策略的制定离不开对系统吞吐与压力的量化描述。以下三个指标构成了限流配置的“语言”:
| 指标 | 全称 | 定义 | 典型场景 |
|---|---|---|---|
| QPS | Queries Per Second | 每秒查询数,衡量接口被调用的频率 | HTTP API、读接口、搜索服务 |
| TPS | Transactions Per Second | 每秒事务数,衡量完整业务处理能力 | 下单、支付、数据库写入 |
| 并发数 | Concurrency | 系统同时处理的请求数量 | 连接池、协程池、数据库连接 |
实践提示: 在 Go 项目中,QPS 与 TPS 的界限有时会模糊。对于单一接口限流,通常直接使用 QPS;对于涉及多个 RPC 调用的业务链路,应优先关注 TPS 与并发数,避免因单个接口限流过严导致整体吞吐下降。
2.2 三种经典限流算法
不同的限流算法在精度、内存开销、应对突发流量等方面各有优劣。下面逐一分析其原理与 Go 实现要点。
2.2.1 计数器算法(固定窗口)
计数器算法是最直观的限流方式:将时间划分为固定窗口(如 1 秒),每个窗口内维护一个计数器,请求到达时计数器加 1,超过阈值则拒绝。
临界问题(Thundering Herd): 固定窗口算法在窗口切换瞬间容易出现流量尖峰。例如阈值 10,窗口 1 最后 0.1 秒 10 个请求,窗口 2 前 0.1 秒 10 个请求,实际 0.2 秒内通过了 20 个请求,导致后端瞬时过载。
2.2.2 滑动窗口算法
滑动窗口是计数器算法的改进版:将窗口细分为多个小格子,随着时间推移,窗口整体向前滑动,统计粒度更细,能有效缓解临界问题。
// Go 滑动窗口限流核心结构示意
type SlidingWindow struct {
mu sync.Mutex
buckets []int64 // 每个格子的请求计数
size int // 格子数量
interval time.Duration // 总窗口大小
threshold int64 // 总窗口阈值
cursor int // 当前写入位置
}
2.2.3 漏桶算法(Leaky Bucket)
漏桶算法将请求视为水滴,以固定速率从桶底漏出,桶满则溢出拒绝。它强制输出为恒定速率,适合需要平滑流量的场景。
2.2.4 令牌桶算法(Token Bucket)
令牌桶算法是目前业界最广泛使用的限流算法。它以固定速率向桶中添加令牌,请求到达时需获取令牌才能通过。相比漏桶,令牌桶允许一定程度的突发流量。
// Go 令牌桶核心逻辑(基于 time.Ticker)
type TokenBucket struct {
rate float64 // 令牌生成速率(个/秒)
capacity int64 // 桶容量
tokens float64 // 当前令牌数
lastTime time.Time // 上次取令牌时间
mu sync.Mutex
}
func (tb *TokenBucket) Allow() bool {
tb.mu.Lock()
defer tb.mu.Unlock()
now := time.Now()
// 先补充令牌
elapsed := now.Sub(tb.lastTime).Seconds()
tb.tokens = math.Min(float64(tb.capacity), tb.tokens + elapsed * tb.rate)
tb.lastTime = now
if tb.tokens >= 1 {
tb.tokens--
return true
}
return false
}
2.3 算法对比与选型建议
| 算法 | 输出速率 | 突发处理 | 内存开销 | 精度 | Go 推荐场景 |
|---|---|---|---|---|---|
| 计数器(固定窗口) | 不保证 | 差(临界问题) | 极低 | 低 | 非关键路径、粗略限流 |
| 滑动窗口 | 较平滑 | 较好 | 中等 | 中 | 需要消除临界问题的场景 |
| 漏桶 | 恒定 | 无 | 低 | 高 | 数据库写入、消息队列生产 |
| 令牌桶 | 可突发 | 优秀 | 低 | 高 | HTTP API、微服务网关 |
选型建议: 在 Go 业务接口限流中,令牌桶 是最通用的选择。它既能平滑流量,又能容忍短时突发,与 HTTP 接口的流量特征高度匹配。Go 标准库
golang.org/x/time/rate 提供了高质量令牌桶实现,可直接用于生产环境。漏桶更适合对输出速率有严格要求的场景,如数据库写入限流。计数器算法仅建议在资源受限或非关键路径上使用。
2.4 并发数限流:另一种视角
除了基于 QPS/TPS 的速率限流,并发数限流 是另一种重要手段。它限制系统同时处理的请求数量,而非单位时间内的请求频率。在 Go 中,常用 channel 或 semaphore.Weighted 实现。
// 基于 channel 的并发数限流
type ConcurrencyLimiter struct {
sem chan struct{}
}
func NewConcurrencyLimiter(max int) *ConcurrencyLimiter {
return &ConcurrencyLimiter{sem: make(chan struct{}, max)}
}
func (cl *ConcurrencyLimiter) Acquire() {
cl.sem <- struct{}{}
}
func (cl *ConcurrencyLimiter) Release() {
<-cl.sem
}
并发数 vs QPS 限流: 并发数限流保护的是系统资源(如 goroutine 池、数据库连接池),QPS 限流保护的是接口处理能力。两者应组合使用:先用并发数限流防止资源耗尽,再用 QPS 限流控制请求速率。例如,设置最大并发 100,同时 QPS 上限 500。