闪电疯狂赛车
91.44M · 2026-03-23
对于后端开发(Java/Go)来说,限流(Rate Limiting)是高可用架构中的核心环节。本文讨论一下关于四种限流常见方案以及对应的一些问题。
这里的窗口是指在一个时间段中,每一个时间段都是已经预设好的静态窗口。这是最简单的实现方式,通常作为初级方案或在性能要求极高的底层系统中使用。
我们一般不使用这个。
滑动窗口是固定窗口的进化版。它将时间划分为多个小格子(Bucket),随着时间推移,最老的格子被丢弃,最新的格子被加入。
优点
缺点
由于滑动窗口无法应对突发流量,我们一般也不使用这个方案。
如果说令牌桶是控制“进水的速率”,那么漏桶控制的就是“出水的速率”。漏桶这种方案主要是用于固定流量速率场景的,有一定的适用场景
原理:所有的请求(水)先进入桶中,桶以固定的速率向外排水(处理请求)。如果桶满了,新进来的水直接溢出(拒绝请求)。
特点:
适用场景:主要用于网络流量整形(Traffic Shaping) ,或者当你下游的服务非常脆弱,绝对不能承受任何瞬时波动时。
令牌桶算法是我们最常用的限流方案之一,接下来我来详细介绍这种算法。
系统以恒定的速率往桶里放令牌,桶满则溢出。请求进来时必须先拿到令牌,否则被限流。
优点
RateLimiter 和 Go 标准库的 golang.org/x/time/rate 均采用此方案。缺点
上面我们说到,系统以恒定的速率往桶里放令牌,桶满则溢出。溢出会有什么后果吗?
在令牌桶算法的设计中, “桶满则溢出”是一个预期的保护机制,并不会导致系统崩溃或逻辑错误。 简单来说,溢出的后果就是“令牌被作废”
1. 限制了“突发流量”的最大上限
令牌桶的一大优势是允许突发(Burst)。如果系统有一段时间没有请求,桶里会攒满令牌。
2. 维持了平均 QPS 的稳定性
令牌放入的速率(Rate)决定了长期的平均吞吐量。
桶的容量其实就是系统能接受的最大冲击流量。
在 Java (Guava RateLimiter) 或 Go (x/time/rate) 的底层实现中,并不会真的有一个线程在不停地“放令牌”然后看它溢出,因为那样太浪费 CPU 了。
它们通常采用 延迟计算 (Lazy Evaluation) 的方式:
当一个请求进来时,根据 当前时间 - 上次请求时间 计算出这段时间内应该产生多少令牌。
这里的 min 函数就是“溢出”的数学表现。 超过 的部分直接被舍弃,不参与计算
这里给出四种方案的对比表格,每种方案都有其使用场景,可以按需选择
| 方案 | 核心目标 | 允许突发流量? | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 固定窗口 | 简单计数 | 否(有临界问题) | 极低 | 极其简单的单机小流量场景 |
| 滑动窗口 | 平滑限流 | 否 | 低 | 大多数微服务接口限流 |
| 令牌桶 | 限制平均速率 | 是 | 中 | API 网关、需要应对瞬时峰值 |
| 漏桶 | 强行平滑流出 | 否 | 中 | 保护及其脆弱的下游资源 |