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,超过阈值则拒绝。

计数器算法(固定窗口)原理 窗口 1 计数: 8 窗口 2 计数: 10 窗口 3 计数: 12 (拒绝) 窗口 4 计数: 5 时间 → ⚠ 临界问题:窗口 1 末尾与窗口 2 开头各 10 个请求 实际 0.2 秒内涌入 20 个请求,但计数器未感知
临界问题(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)

漏桶算法将请求视为水滴,以固定速率从桶底漏出,桶满则溢出拒绝。它强制输出为恒定速率,适合需要平滑流量的场景。

漏桶算法原理 R1 R2 R3 R4 溢出 漏口 恒定速率流出 特点:输出速率恒定,能平滑突发流量 缺点:无法应对突发性业务需求,桶满即丢

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。

2.5 小结