限流算法实战:令牌桶与漏桶原理及 Guava/Sentinel 落地

限流算法是高并发系统的”安全阀”:当瞬时流量超过系统处理能力时,令牌桶与漏桶等算法能以恒定或可控速率放行请求,把超出阈值的流量快速拒绝,从而避免线程耗尽、连接打满、级联雪崩。本文从算法原理讲到 Java 落地,给出可直接复用的代码与选型对照。

一、为什么必须上限流算法

很多故障并不是代码写错,而是”被流量冲垮”。一次大促、一个热点、甚至一个爬虫,都会让 QPS 在几秒内翻十倍。此时若不做流量整形,下游数据库、缓存、第三方接口会同时被打满,进而引发连锁超时与雪崩。限流的本质是牺牲少数请求、保住整体可用性——用可预期的拒绝,换系统不崩。

它与熔断、降级是”黄金三角”:熔断(如 Spring Cloud 微服务治理 中的熔断限流)负责下游异常时快速失败,降级负责异常时返回兜底,而限流负责在入口处把流量”削峰填谷”。

二、四种主流限流算法

2.1 固定窗口计数器

最简单:把时间切成 1 秒的窗口,窗口内计数超过阈值就拒绝。优点是零成本;缺点是窗口临界处会出现两倍突发——第 1 秒末尾和第 2 秒开头各来满额,实际 1 秒内放了 2 倍流量。

2.2 滑动窗口

把窗口再细分为多个小格子,统计”最近 N 秒”的请求数,平滑了固定窗口的临界突变,是计数器到更精确算法的过渡方案。

2.3 漏桶算法(Leaky Bucket)

请求像水一样倒入桶,桶以恒定速率从底部漏出处理。桶满则溢出(拒绝)。它强行把输出速率压成直线,天然削峰,但不允许突发——哪怕系统此刻很空闲,也必须按固定节奏处理。

2.4 令牌桶算法(Token Bucket)

系统以恒定速率往桶里放令牌,请求必须拿到令牌才能通过,桶空则拒绝。因为令牌可以积攒,它允许一定程度的突发流量——这更贴近真实业务(秒杀前排队、开后瞬间涌入)。这也是 Guava RateLimiter 的默认模型。

注意区分层次:Nginx 限流防刷实战 里的漏桶是网关/反向代理层,本文是应用代码层,两者可以叠加形成双层防护。

三、令牌桶 vs 漏桶:一张表看懂

维度令牌桶(Token Bucket)漏桶(Leaky Bucket)
突发流量允许,令牌可积攒不允许,强制恒定速率
流出速率请求驱动,有令牌即放行系统驱动,匀速漏出
适用场景秒杀、API 配额、保护下游严格平滑、报文整形
实现复杂度中(需维护令牌数/时间)低(队列+定时器)

四、Java 实战:Guava RateLimiter

Google Guava 自带生产级令牌桶实现,无需引入重框架,适合单体或单实例限流。

import com.google.common.util.concurrent.RateLimiter;

// 每秒放行 5 个请求(平滑突发)
RateLimiter limiter = RateLimiter.create(5.0);

// 阻塞式获取 1 个令牌(拿不到就等)
limiter.acquire();

// 非阻塞:0.5s 内拿不到立即返回 false
if (limiter.tryAcquire(1, 500, TimeUnit.MILLISECONDS)) {
    handleRequest();
} else {
    return Response.status(429).build(); // 快速失败
}

若接口有”冷启动”压力,用平滑预热模式,让令牌速率在指定时间内从低逐步升到目标值,避免服务刚启动就被打满:

// 每秒 10 个令牌,预热 3 秒(适合缓存/连接池刚建立的场景)
RateLimiter warmup = RateLimiter.create(10.0, 3, TimeUnit.SECONDS);

五、分布式限流:Sentinel 落地

Guava 是单 JVM 维度;多实例集群需要统一口径,Alibaba Sentinel 是业界主流方案,支持 QPS、线程数、关联、链路等多种流控。

@SentinelResource(value = "createOrder", blockHandler = "createOrderBlock")
public Order createOrder(OrderReq req) {
    return orderService.create(req);
}

// 被限流/熔断时的兜底方法,签名需与原方法一致(末尾加 BlockException)
public Order createOrderBlock(OrderReq req, BlockException ex) {
    throw new BizException("当前下单过于频繁,请稍后再试");
}

规则既可在控制台动态推送,也可代码硬编码加载,便于容器启动即生效:

List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setResource("createOrder");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS); // 按 QPS 限流
rule.setCount(100);                          // 单机阈值 100
rules.add(rule);
FlowRuleManager.loadRules(rules);

六、生产避坑清单

现象对策
阈值拍脑袋限太松没用、太紧误杀正常流量先压测取基线,留 20% 余量
单实例限流当全局10 台机器×100=实际 1000 QPS集群用 Sentinel/Redis 令牌桶统一
被限流不返回 429客户端一直重试放大流量明确返回 429+Retry-After
无监控不知道限了多少、是否误伤埋点 block 次数,接入 Prometheus
限流点放错层网关限了应用又限,重复拒绝网关做粗粒度、应用做细粒度

当限流频繁触发且伴随 502/504,往往不是限流本身的问题,而是下游已过载——可结合 Nginx 502/504 排查 一起定位连接与超时链路。

七、真实场景:秒杀系统的多层限流设计

纸上谈兵不如一个真实例子。某电商秒杀接口平时 QPS 仅 50,大促预告后瞬时冲到 8000。如果只在应用层用 Guava 限流,单机阈值即便设 200、10 台机器也只能扛 2000,仍会被冲垮;更糟的是,限流发生在业务逻辑之后,连接和线程在被拒绝前早已被占满。正确做法是从外到内分层设防:① 接入层(Nginx)用漏桶把总入口压到可承受范围;② 网关层(如 Spring Cloud 微服务治理 里的限流)做服务级配额;③ 应用层用 Sentinel 做资源级 QPS 控制;④ 最终落库前,对数据库写操作再套一层令牌桶保护。四层环环相扣,任何一层被击穿都有下一层兜底,系统整体可用性远高于单层限流。

阈值怎么定才不拍脑袋?先拿压测基线(如单机安全 QPS 300),乘以机器数、再留 20% 余量,得到集群阈值;突发倍数则根据业务容忍度设定令牌桶容量。上线后务必把”被限流次数“做成监控指标——灰度期间若误伤正常用户,说明阈值偏紧,应及时回调。限流不是配完就忘,而是要随流量模型持续调参的能力。

八、小结

限流算法没有”最好”,只有”最合适”:单体用 Guava 令牌桶最轻量,集群用 Sentinel 统一口径,网关层再用 Nginx 漏桶兜底。记住核心区别——令牌桶允许突发、漏桶强制匀速。先把单点限流跑通、配上监控,再根据压测数据逐步调阈值,你的系统就能在流量洪峰中稳稳站稳。另外要提醒,限流必须和超时、重试、熔断组合使用才完整——只限流却不设合理超时,请求仍会在队列里堆积、慢慢耗尽线程与连接资源。

上一篇 接口幂等设计:防重复提交与分布式去重实战
下一篇 WebSocket 断线重连与心跳机制实战