令牌桶限流突发流量处理:瞬间峰值超过阈值?预热机制+弹性令牌补充,不误杀正常请求!
朋友公司做秒杀,限流用的是固定窗口计数器——每分钟最多 1000 个请求。每次秒杀开始的瞬间,前 0.5 秒冲进来 800 个请求,计数器直接打满,后面 59.5 秒的正常用户一个都进不来。更要命的是,这个计数器在第 60 秒重置,第 61 秒又是一波 800 个请求冲进来。限流器没有挡住流量洪峰,反而把正常用户拦在了门外。
固定窗口的问题是它不管流量分布。1000 个请求挤在前 5 秒和均匀分布在 60 秒里,对它来说是一样的——反正超了就拒。但业务上,前 5 秒的 800 个秒杀请求是正常的,后面的零散查询也是正常的。你需要的不是"别超过 1000",而是"别把服务器打爆"。
这就是令牌桶擅长的事。
令牌桶和固定窗口的差别在哪
固定窗口的逻辑很粗暴:一个计数器,到时间清零。令牌桶的思路完全反过来——以固定的速度往桶里放令牌,请求来了要先拿到令牌才能通过。
令牌桶模型:
令牌生成器 —→ 每秒放 100 个令牌 —→ [桶容量: 200]
│
请求到达 ────────────────────────────────┤
│
有令牌?—→ 拿走一个,放行 │
没令牌?—→ 拒绝 │
关键区别在于那个"桶"。桶的容量决定了你能存多少令牌,也决定了你能处理的突发流量有多大。
假设桶容量是 200,生成速率是每秒 100 个。如果前 5 秒没人来,桶里攒满了 200 个令牌。第 6 秒突然来了一波 200 个请求,全部放行——因为桶里有存货。但第 200 个请求之后,桶空了,新的请求只能等令牌慢慢生成,每秒最多放行 100 个。
这就是令牌桶处理突发流量的方式:用平时攒下的令牌吸收峰值,峰值过后回到稳定速率。
预热机制:冷启动时别把桶攒满
令牌桶有一个常见的变种叫"预热桶"(Warm Up)。核心区别是——刚启动的时候,令牌的生成速率不是满的,而是从低到高慢慢爬升到目标速率。
为什么需要这个?假设你的服务刚启动,JVM 还没预热,连接池也还没建立完。这时候如果桶里已经攒满了令牌,一波突发流量直接打过来,服务可能还没准备好就被冲垮了。
预热桶的逻辑:
冷启动阶段:
第 1 秒:生成 20 个令牌(20% 速率)
第 2 秒:生成 40 个令牌(40% 速率)
第 3 秒:生成 60 个令牌(60% 速率)
...
第 10 秒:生成 100 个令牌(100% 速率)← 达到目标
正常阶段:
每秒 100 个令牌,桶容量 200,正常运行
预热阶段的时长和曲线可以配置。Guava 的 RateLimiter 就内置了预热模式:
RateLimiter limiter = RateLimiter.create(
100, // 稳定速率:每秒 100 个
10, // 预热时间:10 秒
TimeUnit.SECONDS
);
前 10 秒它不会满速率放行,而是从三分之一速率逐渐爬升。这样就给了下游服务一个缓冲时间来预热连接池、加载缓存、JIT 编译热点代码。
弹性令牌补充:别让拒绝率跳得太快
标准令牌桶有个问题:速率是固定的。每秒 100 个就是 100 个,不会多也不会少。
但真实的业务流量是有波动的。中午 12 点订单量是凌晨 3 点的十倍。如果你按平均值设速率,高峰期限流太狠;按峰值设,平时基本用不上。
弹性令牌补充的思路是:允许令牌生成速率在一个范围内浮动,根据当前桶里的存量来动态调整。
弹性策略:
桶里剩余很多(> 80% 容量)→ 降速(说明流量不大,省资源)
桶里剩一半(40~80%)→ 正常速率
桶里快空了(< 40%)→ 提速(流量高,多补一点令牌)
伪代码:
class ElasticTokenBucket:
baseRate = 100 // 基本速率
maxRate = 200 // 最大速率
capacity = 200
refill():
fillLevel = tokens / capacity // 当前水位
if fillLevel > 0.8:
rate = baseRate * 0.5 // 水位高 → 流量低 → 降速
elif fillLevel < 0.4:
rate = min(maxRate, baseRate * (1 + (1 - fillLevel)))
// 水位低 → 流量高 → 提速
else:
rate = baseRate // 正常
tokens += rate * interval
tokens = min(tokens, capacity)
注意弹性提速是有上限的——maxRate。不能让令牌无限生成,否则限流就形同虚设了。一般 maxRate 设为 baseRate 的 1.5 到 2 倍。
实战:预热 + 弹性 + 固定桶的组合方案
把三个机制串起来,一个生产级的令牌桶限流器:
冷启动阶段(0~10s):
预热机制 → 速率从 30 逐渐爬升到 100
正常运行阶段(10s 后):
弹性机制 → 根据桶水位在 50~150 之间浮动
桶容量 200 → 可吸收 200 个突发请求
配置上:
baseRate = 100 // 稳定速率
maxRate = 150 // 弹性上限(1.5 倍)
warmupPeriod = 10s // 预热时长
capacity = 200 // 桶容量(baseRate × 2)
三个参数决定了整个限流器的行为。调参的时候建议一次只改一个,观察效果后再调下一个。先定桶容量,再设预热时长,最后调弹性上限。
Guava RateLimiter 和 Sentinel 怎么选
Java 生态里的令牌桶实现主要有两个:
Guava RateLimiter:单机、轻量、API 简洁。适合服务内部限流——比如限制某个方法的调用频率。但它不支持分布式,多个实例之间不共享桶状态。
Sentinel:阿里开源的流量治理组件,支持分布式限流、熔断降级、热点参数限流。功能更全但也更重。如果你的项目已经有 Nacos 或者 Apollo,Sentinel 可以直接对接动态配置。
对于大多数场景,单机 Guava RateLimiter 就够用。因为限流的目的是保护当前这台机器不被自己的流量打爆,不一定要全局统一限流。如果你的服务有 5 个实例,每个实例限 100 QPS,即使没有分布式协调,整体也不会超过 500 QPS——这个精度在大多数场景下已经足够了。
几个容易踩的配置坑
桶容量不是越大越好
桶容量 1000,速率 100/秒,意味着你在流量低谷期最多能攒 1000 个令牌。下次突发来了,1000 个请求一次性放过去,可能直接把下游打爆。
桶容量一般设为稳定速率的 1.5~3 倍。比如速率 100/秒,桶容量 200 基本够了。
预热时长要跟着实际启动时间走
如果你的服务启动后需要 3 秒加载缓存、5 秒完成 JIT 预热,那预热时长至少设 10 秒。设太短了没效果,设太长了正常流量都被限。
弹性上限不要超过下游的承受能力
弹性提速确实能减少误杀,但如果你的下游数据库最大只能扛 200 QPS,弹性上限设成了 300,那限流器自己就把数据库打爆了。弹性上限要以上游服务调用链里最薄弱的环节为准。
总结
令牌桶面对突发流量的优势在于——它攒的是"时间",而不是"次数"。平时没人用的时候,令牌持续积累;突发来了,用积攒的令牌吸收峰值。
三个机制各管一摊:
- 预热:冷启动阶段别让令牌生产太快,给服务留热身时间
- 弹性补充:根据桶水位动态调速率,高峰多补、低谷省着
- 桶容量:设好上限,控制最大的瞬时吞吐量
三者结合,既能扛住秒杀级别的突发流量,又不会在平时把下游压垮。
实际落地的话,Guava RateLimiter 自带了预热模式,弹性补充需要自己扩展一下,但核心逻辑就那么几十行代码,不复杂。
标题:令牌桶限流突发流量处理:瞬间峰值超过阈值?预热机制+弹性令牌补充,不误杀正常请求!
作者:jiangyi
地址:http://jiangyi.space/articles/2026/06/03/1780410593926.html
公众号:服务端技术精选
评论