2. 限流基础概念
2.1 什么是限流
限流(Rate Limiting)是一种通过控制单位时间内允许通过的请求数量,来保护系统不被突发流量冲垮的防护手段。当请求速率超过预设阈值时,多余的请求会被直接拒绝、排队等待或降级处理。
限流的核心目标可以概括为三点:
- 保护后端资源:防止数据库、缓存、微服务等下游组件因过载而崩溃。
- 保障服务质量:确保有限的系统资源能被公平分配给合法用户,避免少数高并发请求挤占所有连接。
- 成本控制:在云原生环境下,限流可以防止因流量异常导致的计算资源费用飙升。
2.2 常见限流算法概述
不同的业务场景对限流的精度、内存开销和突发处理能力有不同要求。下表对比了四种主流限流算法的核心差异:
| 算法 | 核心原理 | 精度 | 内存开销 | 是否支持突发 | 典型场景 |
|---|---|---|---|---|---|
| 计数器 | 固定时间窗口内累加计数,超限则拒绝 | 低(存在临界突变) | 极低(1个计数器) | 否 | 简单接口限流、非关键路径 |
| 滑动窗口 | 将时间窗口细分为多个小格,滑动统计 | 中(取决于分片数) | 中(需存储多个子窗口计数) | 否 | API网关、通用HTTP限流 |
| 漏桶 | 请求先进入队列,以固定速率流出处理 | 高(严格平滑) | 中(需维护队列) | 否(强制削峰填谷) | 数据库写入限流、消息生产限速 |
| 令牌桶 | 以固定速率生成令牌,请求需获取令牌才能通过 | 高 | 低(仅维护令牌数和时间戳) | 是(允许一定突发) | 微服务限流、网络流量整形 |
2.3 四种算法详解
2.3.1 计数器算法
计数器算法是最直观的限流实现。它将时间划分为固定的窗口(如1秒),每个窗口内维护一个计数器。请求到达时计数器加1,若计数器超过阈值则拒绝该请求。窗口结束时计数器归零。
典型问题——临界突变:假设阈值是100次/秒。如果在第1秒的最后10ms内通过了100个请求,紧接着在第2秒的前10ms内又通过了100个请求,那么这20ms内实际通过了200个请求,形成了流量尖峰。
// 计数器限流伪代码
type CounterLimiter struct {
threshold int64
count int64
windowStart time.Time
mu sync.Mutex
}
func (c *CounterLimiter) Allow() bool {
c.mu.Lock()
defer c.mu.Unlock()
now := time.Now()
if now.Sub(c.windowStart) >= time.Second {
c.count = 0
c.windowStart = now
}
if c.count >= c.threshold {
return false
}
c.count++
return true
}
2.3.2 滑动窗口算法
滑动窗口算法将固定窗口进一步细分为多个小格子(如将1秒分为10个100ms的格子)。每个格子独立计数,窗口随着时间推移向前滑动,统计当前窗口内所有格子的累计值。格子数量越多,精度越高,但内存开销也线性增长。
2.3.3 漏桶算法
漏桶算法将请求比作水滴,系统是一个底部有漏洞的桶。水滴以任意速率流入桶中,但桶底以固定的速率漏水(处理请求)。如果桶满了,后续的水滴(请求)就会溢出(被拒绝)。
漏桶的核心特性是强制平滑流量——无论入口流量多么不均匀,出口速率始终恒定。这对于保护数据库连接池、消息队列等对写入速率敏感的资源非常有效。
// 漏桶限流伪代码(基于队列)
type LeakyBucket struct {
capacity int // 桶容量
rate time.Duration // 每个请求的处理间隔
queue chan struct{}
ticker *time.Ticker
}
func (l *LeakyBucket) Allow() bool {
select {
case l.queue <- struct{}{}:
return true
default:
return false // 桶满,拒绝
}
}
// 后台协程以固定速率从队列中取出请求
func (l *LeakyBucket) Start() {
go func() {
for range l.ticker.C {
<-l.queue
// 处理请求
}
}()
}
2.3.4 令牌桶算法
令牌桶算法是目前应用最广泛的限流算法。系统以固定的速率向桶中添加令牌,桶有最大容量限制。每个请求到达时,需要从桶中获取一个令牌才能被处理。如果桶中没有令牌,请求被拒绝或等待。
与漏桶不同,令牌桶允许一定程度的突发流量:如果桶中积累了足够的令牌(例如系统空闲时),短时间内可以处理大量请求。
// 令牌桶限流伪代码(Go标准库rate包简化版)
type TokenBucket struct {
rate float64 // 每秒生成的令牌数
capacity float64 // 桶容量
tokens float64 // 当前令牌数
lastTime time.Time // 上次更新时间
mu sync.Mutex
}
func (t *TokenBucket) Allow() bool {
t.mu.Lock()
defer t.mu.Unlock()
now := time.Now()
// 先补充令牌
elapsed := now.Sub(t.lastTime).Seconds()
t.tokens = math.Min(t.capacity, t.tokens + elapsed * t.rate)
t.lastTime = now
if t.tokens < 1 {
return false
}
t.tokens--
return true
}
2.4 限流与背压的关系
背压(Back Pressure)是一种从下游向上游传递压力信号的机制。当下游组件处理能力不足时,它会主动告知上游“请放慢发送速度”,从而形成一条从消费者到生产者的负反馈链路。
限流与背压的核心区别在于控制方向:
- 限流:在入口处主动拒绝请求,是一种前置防御策略。系统在流量进入之前就设定好阈值,超出部分直接丢弃。
- 背压:根据下游的实际处理能力动态调节上游速率,是一种后置反馈策略。系统不直接丢弃请求,而是让上游等待或降速。
2.5 限流算法的选型决策树
在实际项目中,选择哪种限流算法通常取决于以下三个维度:
- 是否需要支持突发流量?是 → 令牌桶;否 → 漏桶或滑动窗口。
- 对精度要求有多高?毫秒级 → 滑动窗口(细粒度分片)或令牌桶;秒级 → 计数器(需注意临界突变)。
- 内存是否敏感?嵌入式/IoT设备 → 计数器或令牌桶;服务端API网关 → 滑动窗口或令牌桶。
golang.org/x/time/rate 提供了开箱即用的令牌桶实现,底层基于 time.Time 和浮点数计算,无需额外存储结构,是微服务限流的首选。对于需要精确控制窗口边界的场景(如秒杀系统),建议使用滑动窗口算法,并配合Redis的Sorted Set实现分布式限流。