任何系统的处理能力都有上限。限流的本质是将请求到达速率压制在系统容量之内——代价是少量请求被拒绝或延迟,但这往往是在稳定性与可用性之间取得最优解的必要手段。
TL;DR
- 限流 = 限制请求速率,以可控的拒绝/排队换系统不被打垮。
- 固定窗口实现最简,但不够平滑,且无法防御窗口边界的突发流量。
- 滑动窗口把时间粒度切得更细,统计更精确,仍有平滑性不足的问题。
- 漏桶以固定速率漏出,能严格控速但无法吸收突发;实践中很少单独使用。
- 令牌桶既能限制平均速率又能吸收突发,是生产中最常用的方案。
- 限流对象常见 IP、业务 ID、个性化策略;Sentinel 还支持基于调用关系与热点参数限流。
- 单机用 Guava / Bucket4j / Resilience4j;分布式用 Redis+Lua、Redisson 或网关层限流。
- §4.1 的 Lua 脚本是固定窗口的原子实现,不是令牌桶——真正的分布式令牌桶用 Redisson
RRateLimiter或 redis-cell(CL.THROTTLE)。 - 限流要分层:网关层拦全局洪峰、服务层护单接口容量、依赖调用层守下游配额,三层各管一段。
Table of contents
Open Table of contents
1. 四种经典限流算法
以下依次分析四种主流算法的原理与核心取舍。
1.1 固定窗口
固定窗口算法将时间划分为大小相等的窗口,在每个窗口内统计并限制请求数量。
以”接口每分钟最多访问 33 次”为例,实现思路如下:
- 将时间划分为固定大小的窗口(此处为 1 分钟)。
- 用变量
counter记录当前窗口内的请求数,初始值为 0。 - 每处理一个请求后
counter+1;当counter=33后,拒绝该窗口内的后续请求。 - 窗口结束后将
counter清零,进入下一轮计数。
固定窗口内计数达上限就拒绝,窗口结束再清零——实现简单但不平滑。
优点:实现简单,易于理解。
缺点:
- 限流不够平滑。 例如限制每分钟 30 次,若前 30 秒内 30 个请求已到达,后续 30 秒将无法处理任何请求,用户体验极差。
- 无法防御窗口边界的突发流量。 若接口限制每分钟 1000 次,前 55 秒无请求,最后 1 秒突然涌入 1000 个请求,系统可能在瞬间被击垮。
1.2 滑动窗口
滑动窗口是固定窗口的改进:将一个大窗口切分为若干小格子,计数窗口随时间连续滑动。
以”每分钟 60 次”为例:将 1 分钟拆分为 60 个 1 秒的小格,每隔 1 秒向前滑动一次,统计当前窗口内所有格子的请求总数。格子划分越细,滑动越平滑,限流统计越精确。
格子越细滚动越平滑,比固定窗口更能扛突发,但仍非真正匀速。
优点:
- 相比固定窗口,能更好地应对突发流量。
- 粒度更细,限流控制更精确。
缺点:
- 仍存在限流不够平滑的问题(同一小格内请求可集中到达)。
- 实现和维护比固定窗口更复杂。
1.3 漏桶
请求以任意速率注入”桶”中,以固定速率从桶底漏出处理;桶满时新请求被丢弃。实现上对应一个请求队列加定时消费(与消息队列削峰/限流的思路一致)。
任意速率注入、固定速率漏出;桶满就丢——能控速但吃不了突发。
优点:
- 实现简单。
- 严格控制输出速率,能有效防止网络拥塞和系统过载。
缺点:
- 无法吸收突发:即便系统当前空闲,也只能以固定速率处理请求,资源利用率偏低。
- 持续高速注入时桶会始终处于满载状态,导致大量新请求被丢弃,服务质量下降。
- 实际业务场景中,漏桶算法很少单独使用。
1.4 令牌桶
系统以固定速率向桶中放入令牌,桶满则停止放入。每个请求在处理前须先从桶中取走一个令牌,取不到则拒绝或等待。
按速率放令牌、请求先取令牌再处理——限平均速率又能吃突发。
优点:
- 同时限制平均速率和突发流量——桶中积攒的令牌可吸收短时脉冲请求。
- 令牌生成速率可动态调整,灵活性高。
缺点:
- 令牌生成速率和桶容量配置不合理时,可能出现大量请求被丢弃或系统过载。
- 相比固定/滑动窗口,实现略复杂。
2. 限流对象与维度
确定限流算法后,还需要明确针对什么维度限流。常见维度:
- IP:适用面广、实现简单,但需注意代理或 NAT 环境下真实 IP 的正确获取(常用
X-Forwarded-For,需防伪造)。 - 业务 ID:更精准,如按用户 ID、租户 ID 限流,适合多租户或差异化服务场景。
- 个性化策略:根据用户属性或系统运行指标动态调整,如 VIP 用户放行、普通用户限流;或当系统负载升高时自动收紧阈值。
此外,阿里 Sentinel 还支持 基于调用关系的限流(包括调用方限流、调用链入口限流、关联流量限流等)以及更细粒度的 热点参数限流(实时统计热点参数并对其资源调用进行流量控制)。
实际项目中,多种维度可以组合使用。
3. 单机限流实现
单机限流适用于单体架构,也可作为分布式系统中每个节点的本地兜底。
3.1 Guava RateLimiter
单机限流可直接使用 Google Guava 的 RateLimiter,底层基于令牌桶算法,支持突发流量吸收。
Guava 地址:https://github.com/google/guava
除基础令牌桶(平滑突发限流)外,RateLimiter 还提供平滑预热限流——预热期间速率从低逐步爬升到目标值,适合冷启动场景。
引入依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
</dependency>
示例一:平滑突发限流
import com.google.common.util.concurrent.RateLimiter;
public class RateLimiterDemo {
public static void main(String[] args) {
// 1s 放 5 个令牌到桶里也就是 0.2s 放 1个令牌到桶里
RateLimiter rateLimiter = RateLimiter.create(5);
for (int i = 0; i < 10; i++) {
double sleepingTime = rateLimiter.acquire(1);
System.out.printf("get 1 tokens: %ss%n", sleepingTime);
}
}
}
输出:
get 1 tokens: 0.0s
get 1 tokens: 0.188413s
get 1 tokens: 0.197811s
get 1 tokens: 0.198316s
get 1 tokens: 0.19864s
get 1 tokens: 0.199363s
get 1 tokens: 0.193997s
get 1 tokens: 0.199623s
get 1 tokens: 0.199357s
get 1 tokens: 0.195676s
示例二:平滑预热限流
import com.google.common.util.concurrent.RateLimiter;
import java.util.concurrent.TimeUnit;
public class RateLimiterDemo {
public static void main(String[] args) {
// 1s 放 5 个令牌到桶里也就是 0.2s 放 1个令牌到桶里
// 预热时间为3s,也就说刚开始的 3s 内发牌速率会逐渐提升到 0.2s 放 1 个令牌到桶里
RateLimiter rateLimiter = RateLimiter.create(5, 3, TimeUnit.SECONDS);
for (int i = 0; i < 20; i++) {
double sleepingTime = rateLimiter.acquire(1);
System.out.printf("get 1 tokens: %sds%n", sleepingTime);
}
}
}
输出:
get 1 tokens: 0.0s
get 1 tokens: 0.561919s
get 1 tokens: 0.516931s
get 1 tokens: 0.463798s
get 1 tokens: 0.41286s
get 1 tokens: 0.356172s
get 1 tokens: 0.300489s
get 1 tokens: 0.252545s
get 1 tokens: 0.203996s
get 1 tokens: 0.198359s
一句话:单机场景 Guava
RateLimiter够用;需要限流 + 熔断协同,优先考虑 Resilience4j。
3.2 Bucket4j
Bucket4j 是专注令牌桶/漏桶算法的限流库,功能比 Guava 更全面:同时支持单机和分布式限流,并可集成 Prometheus + Grafana 进行可观测性监控。
Bucket4j 地址:https://github.com/vladimir-bukhtoyarov/bucket4j
Spring Cloud Gateway 早期版本的单机限流即基于 Bucket4j 实现,后迁移至 Resilience4j。
3.3 Resilience4j
Resilience4j 是轻量级容错框架,灵感来自 Hystrix。Netflix 宣布停止积极维护 Hystrix 后,Spring 官方和 Netflix 均推荐以 Resilience4j 替代。
Resilience4j 地址: https://github.com/resilience4j/resilience4j
Resilience4j 将限流、熔断、负载保护、自动重试等高可用能力集成在同一框架内,生态成熟,被众多网关采用。在绝大多数生产场景下,Resilience4j 是更完整的选择;简单限流场景下 Guava 或 Bucket4j 同样适用。
4. 分布式限流实现
单机限流各自维护本地计数器,互不通信。一旦服务部署多实例,单机方案就会失真:假设下游 API 的全局配额是 500 QPS,10 台实例每台各自限流 500 QPS,全局实际放行量可达 10×500=5000 QPS——是设计阈值的 10 倍,限流形同虚设。要保住”全局阈值”这个约束,必须把计数状态集中存放,多台实例共享同一份计数,这正是分布式限流要解决的问题。常见方案:
- 借助中间件限流:使用 Sentinel(内置集群流控模式,由 Token Server 统一发放配额),或基于 Redis 自行实现限流逻辑。
- 网关层限流:在网关统一拦截,通常也依赖中间件。如 Spring Cloud Gateway 的
RedisRateLimiter基于 Redis+Lua,也可整合 Sentinel。
4.1 Redis + Lua:为什么必须原子化
把”读计数 → 判断是否超限 → 写回计数”这三步拆成多条独立 Redis 命令执行,会出现经典的 check-then-act 竞态:假设阈值为 100,当前计数 count=99,两个并发请求几乎同时执行 GET count,都读到 99,都判断”99+1 ≤ 100”通过检查,于是都执行 INCR——最终 count=101,已经超过阈值却谁都没被拒绝。请求量越大、网络延迟越明显,这类竞态命中的概率就越高。
解决办法是把”读取、判断、自增、设置过期”打包进一段 Lua 脚本,交给 Redis 原子执行:
-- 注意:这是「固定窗口计数器」的原子实现,不是令牌桶!
-- KEYS[1] = 限流 key;ARGV[1] = 窗口内最大请求数;ARGV[2] = 窗口过期秒数
local current = tonumber(redis.call('GET', KEYS[1]) or '0')
if current + 1 > tonumber(ARGV[1]) then
return 0 -- 超限,拒绝
end
redis.call('INCR', KEYS[1])
if current == 0 then
redis.call('EXPIRE', KEYS[1], ARGV[2]) -- 仅在窗口新建时设置过期
end
return 1 -- 放行
务必看清:上面这段脚本对应的是 §1.1 的固定窗口算法(在一个会过期的 key 上做原子计数),不是令牌桶——它不会按速率补充配额,也无法吸收跨窗口的突发。很多文章把这类脚本称作”Redis 令牌桶”,是不准确的。真正的分布式令牌桶需要在脚本里根据时间差补充令牌(见 §4.2),或直接用现成组件(见 §4.3)。
这段固定窗口脚本能生效的原因是 Redis 单线程执行命令:一段 Lua 脚本在被调度执行期间,其他客户端的命令(包括另一个并发请求的 Lua 脚本)不可能插进来打断它,脚本内的”读—判断—写”在 Redis 视角下是一个不可分割的整体,天然消除了上面的竞态。相比”多条命令 + 客户端自己判断”,Redis+Lua 还额外减少了网络往返——四步逻辑一次 EVAL 调用完成,而不是三四次独立的网络请求。
4.2 Redis + Lua:真正的分布式令牌桶
要在 Redis 上实现令牌桶(而非固定窗口),核心是把桶的状态(当前令牌数、上次补充时间)存进 Redis,每次请求时按”距上次补充经过的时间 × 速率”惰性补充令牌,再判断是否够扣。整段逻辑同样打进一段 Lua 脚本原子执行:
-- 分布式令牌桶:惰性补充 + 原子扣减
-- KEYS[1] = 令牌数 key;KEYS[2] = 上次补充时间戳 key
-- ARGV[1] = 桶容量;ARGV[2] = 每秒生成令牌数;ARGV[3] = 当前时间(秒,含小数);ARGV[4] = 本次请求令牌数
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local need = tonumber(ARGV[4])
local tokens = tonumber(redis.call('GET', KEYS[1]) or capacity)
local last = tonumber(redis.call('GET', KEYS[2]) or now)
-- 按经过的时间补充令牌,上限为桶容量
local filled = math.min(capacity, tokens + (now - last) * rate)
local allowed = 0
if filled >= need then
filled = filled - need
allowed = 1
end
-- 令牌耗尽到重新填满的最长时间作为 TTL,避免冷 key 常驻
local ttl = math.ceil(capacity / rate) + 1
redis.call('SET', KEYS[1], filled, 'EX', ttl)
redis.call('SET', KEYS[2], now, 'EX', ttl)
return allowed -- 1 放行,0 拒绝
相比固定窗口脚本,令牌桶脚本多维护了一个”上次补充时间”,从而能按速率平滑补充、跨窗口吸收突发——这正是 §1.4 令牌桶的两大优势。生产中不必自己维护脚本,Redis 官方模块 redis-cell 直接提供了 CL.THROTTLE 命令(基于 GCRA 泛化信元速率算法,本质是令牌桶的等价实现),一条命令即可完成限流并返回剩余配额与建议重试时间:
# CL.THROTTLE key 桶容量 填充令牌数 填充周期(秒) 本次请求令牌数
> CL.THROTTLE bid:campaign:123 100 200 1 1
1) (integer) 0 # 0=放行, 1=拒绝
2) (integer) 100 # 桶容量
3) (integer) 99 # 剩余令牌
4) (integer) -1 # 被拒时需等待秒数(放行为 -1)
5) (integer) 1 # 桶重新填满需等待秒数(可直接作为 Retry-After)
4.3 Redisson
不想手写 Lua 脚本,可以直接使用 Redisson 的 RRateLimiter,底层已封装了 Lua + 令牌桶算法。
Redisson 是开源的 Java Redis 客户端,内置分布式锁、延迟队列、限流器等常用组件,支持 Redis 单机、Sentinel、Cluster 等多种部署模式。
RRateLimiter 使用示例:
// 创建一个 Redisson 客户端实例
RedissonClient redissonClient = Redisson.create();
// 获取一个名为 "javaguide.limiter" 的限流器对象
RRateLimiter rateLimiter = redissonClient.getRateLimiter("javaguide.limiter");
// 尝试设置限流器的速率为每小时 100 次
// RateType 有两种,OVERALL是全局限流,ER_CLIENT是单Client限流(可以认为就是单机限流)
rateLimiter.trySetRate(RateType.OVERALL, 100, 1, RateIntervalUnit.HOURS);
获取许可:
// 获取一个许可,如果超过限流器的速率则会等待
// acquire()是同步方法,对应的异步方法:acquireAsync()
rateLimiter.acquire(1);
// 尝试在 5 秒内获取一个许可,如果成功则返回 true,否则返回 false
// tryAcquire()是同步方法,对应的异步方法:tryAcquireAsync()
boolean res = rateLimiter.tryAcquire(1, 5, TimeUnit.SECONDS);
4.4 单机限流够不够:一句话判断
只要该接口部署了多台实例、且需要一个全局共享阈值,单机限流就不可信——除非业务能接受”全局吞吐 = 单机阈值 × 实例数”这种会随扩容而漂移的软约束。具体决策:
- 单实例部署,或多实例但天然按用户/分片隔离(无需全局共享阈值)→ 单机限流够用(Guava / Bucket4j / Resilience4j)。
- 多实例共享同一个全局阈值(如”这个下游 API 全局只能调 500 QPS”)→ 必须上分布式限流(Redis+Lua、Redisson 或 Sentinel 集群流控)。
- 两者都要:中心限流器控制全局配额,每个实例再叠加一层本地限流兜底——中心存储(Redis/Sentinel Token Server)故障或延迟升高时,本地限流仍能提供最后一道防线,避免”限流器挂了等于没限流”。
5. 分层限流:网关、服务、依赖三层各管一段
限流不是在某一个点设一个阈值就完事——一个成熟系统会在多个层次各设一道,职责互不重叠。把所有限流都堆在网关,则服务内部某个慢接口仍能自己把自己拖垮;只在服务层限流,则恶意刷量的洪峰会先打穿网关的连接资源。三层各管一段:
| 层次 | 限流对象 | 典型配额依据 | 常用手段 |
|---|---|---|---|
| 网关层 | 全站/租户/IP 的总入口流量 | 集群总容量、防刷、DDoS 缓冲 | Nginx limit_req、Spring Cloud Gateway RedisRateLimiter、Sentinel 网关流控 |
| 服务层 | 单个接口/资源的 QPS 或并发 | 该接口压测饱和点 × 安全系数 | Sentinel 资源规则、Resilience4j RateLimiter |
| 依赖调用层 | 对某个下游/第三方的出向调用速率 | 下游承诺的配额(如”500 QPS”) | 分布式令牌桶(Redis+Lua/Redisson)、客户端侧限流器 |
- 网关层关注”整体进来的量别超过集群能扛的”,粒度粗、覆盖广,是抵御突发和恶意流量的第一道闸;它不了解也不该了解每个下游的精确配额。
- 服务层关注”这个接口自身别被打爆”,阈值来自该接口的压测数据(参见 性能测试),是防止单接口拖垮整个服务实例的关键。
- 依赖调用层关注”我方对下游的调用别超过对方给的配额”,是出向保护——它和依赖韧性里的超时、熔断是同一条防线上的相邻环节(限流在最前,超时/熔断在其后)。
请求路径上三道闸各管一段:网关拦全局洪峰、服务护单接口容量、依赖调用守下游配额——叠加而非替代。
一句话:网关层护”进来的总量”,服务层护”单接口容量”,依赖调用层护”出去打下游的速率”——三者叠加而非互相替代。
6. 选型决策与反模式
6.1 算法怎么选
| 场景 | 推荐算法 | 理由 |
|---|---|---|
| 需要严格均匀输出速率(对下游调用保护) | 漏桶 | 固定输出速率,不允许突发 |
| 通用限流,允许少量突发 | 令牌桶 | 兼顾平均速率与突发吸收,生产最常用 |
| 实现简单、准确度要求不高 | 固定/滑动窗口 | 易于理解和维护 |
| 对接第三方 API(有每分钟配额) | 滑动窗口 | 细粒度更能防止配额超限 |
6.2 框架怎么选
| 部署模式 | 推荐方案 |
|---|---|
| 单机,只需限流 | Guava RateLimiter |
| 单机,需要限流 + 熔断 + 重试 | Resilience4j |
| 分布式,Java 系 | Redisson RRateLimiter 或 Redis+Lua |
| 网关层统一限流 | Spring Cloud Gateway RedisRateLimiter 或 Sentinel |
| 大规模微服务 | Sentinel(支持集群模式、热点参数、QPS/线程数双维度) |
6.3 常见反模式
-
在数据库层做限流:用行锁实现「同时只允许一个请求」,高并发下锁竞争极大,反而成为瓶颈;应移到 Redis 原子操作层。
-
阈值脱离压测数据:拍脑袋设定「每秒 100 次」,但未验证系统真实容量。阈值设低则正常请求被拒,设高则形同虚设;应以压测饱和点 × 安全系数(0.7–0.8)作为基准,参见 性能测试。
-
全局单一粒度限流:对整个
/api/**统一限流,粒度过粗——低价值接口消耗配额,核心高价值接口反被误杀;应按接口重要性分层设置阈值。 -
忽略限流后的用户体验:直接返回
429 Too Many Requests但不携带Retry-After头,客户端无法知道何时重试,频繁轮询反而加重压力;应规范错误码并携带重试建议。 -
单机限流用于分布式场景:多实例部署时,每台机器各自按 100 QPS 限流,全集群实际通过 N×100 QPS,失去保护意义;多实例必须使用集中式分布式限流(Redis / Sentinel)。
小结
- 限流的本质是把到达速率压制在系统容量之内,用可控的拒绝/排队换系统不被打垮。
- 四种算法各有取舍:固定/滑动窗口实现简单,漏桶严格控速,令牌桶兼顾平均速率与突发吸收、生产最常用。
- §4.1 的 Lua 脚本是固定窗口的原子实现,不是令牌桶——真正的分布式令牌桶要按时间差惰性补充令牌(§4.2),或直接用 Redisson
RRateLimiter/ redis-cell。 - 限流要分层:网关层拦全局洪峰、服务层护单接口容量、依赖调用层守下游配额,三层各设阈值、互不替代。
- 限流器自身不能成为瓶颈:绝不用带锁的 DB 计数做高频限流,限流状态必须与业务存储隔离,并想清楚故障时 fail-open 还是 fail-close。
系列其他篇:
参考
- 服务治理之轻量级熔断框架 Resilience4j:https://xie.infoq.cn/article/14786e571c1a4143ad1ef8f19
- 超详细的 Guava RateLimiter 限流原理解析:https://cloud.tencent.com/developer/article/1408819
- 实战 Spring Cloud Gateway 之限流篇:https://www.aneasystone.com/archives/2020/08/spring-cloud-gateway-current-limiting.html
- 详解 Redisson 分布式限流的实现原理:https://juejin.cn/post/7199882882138898489
- 一文详解 Java 限流接口实现 - 阿里云开发者:https://mp.weixin.qq.com/s/A5VYjstIDeVvizNK2HkrTQ
- 分布式限流方案的探索与实践 - 腾讯云开发者:https://mp.weixin.qq.com/s/MJbEQROGlThrHSwCjYB_4Q