我印象特别深的一次线上事故,是某个大促活动刚开始那几分钟,一个稳定跑了快两年的订单接口,突然被外部脚本几波狂扫,QPS从平常的几百直接冲到两万多。数据库连接池先被打满,紧接着下游库存服务开始连环超时,不到五分钟整条下单链路全挂了。事后复盘,代码里的超时、重试、缓存其实都做了,缺的恰恰是入口层一道限流。那时候我才真正意识到,限流不是一个“限制用户”的工具,而是系统在流量失控时保命的东西。
这篇我会把限流最经典的4种算法——固定窗口、滑动窗口、漏桶、令牌桶——从原理到Java实现完整过一遍,每种算法附上可运行的代码,并且重点说清楚生产环境里容易踩的坑。无论你是在准备Java面试,还是正在给核心服务做高可用改造,这篇都应该对你有实际帮助。
1. 服务雪崩的传导路径:限流到底在护哪个环节
1.1 雪崩不是瞬间发生的,是一层一层被击穿的
很多人以为雪崩是“服务器突然崩溃”,实际上它更像一个多米诺骨牌过程。你的服务A平时扛1000 QPS没问题,突然来了5000 QPS,单机线程池开始排队。线程池排满之后,新的请求进不来,只能等超时。这时候如果服务A要调用服务B,B的响应时间也在变慢,A的这些线程就会一直占着不释放。线程越积越多,CPU和内存被吃掉,A开始大量报错,而调用A的上游又因为依赖A超时而出现同样的线程堆积。整个链路就是这样被一层层拖垮的。
限流解决的问题,就是“不让超过系统承载能力的流量进入处理环节”。它不是等系统扛不住了再拒绝,而是在入口处提前判断这个请求该不该放行。所以限流经常被部署在外层网关、API入口或下游服务调用的前置逻辑里。
1.2 限流、熔断、降级,三者分工不太一样
面试里经常把限流、熔断、降级混在一起问,但它们的职责是不同的:
- 限流:控制单位时间内的请求数量。系统只能处理1000 QPS,我就只放1000进来,多余的直接拒掉。核心是“不让入口被打爆”。
- 熔断:当依赖的下游服务连续出错时,快速断开调用链路,不再请求已经故障的服务。核心是“不让故障范围继续扩散”。
- 降级:在系统压力过大时,主动牺牲一些非核心功能,比如关闭推荐流、返回兜底数据。核心是“用次要功能保主要功能”。
限流是第一道闸门,通常负责挡住外部流量;熔断和降级更多是内部自我保护。一个完整的高可用方案,通常三者都要有,但限流是最基础的一道防线。
1.3 评判限流算法好用不好用,主要看三件事
看完四种算法之后你会发现,它们的核心差异其实就是三件事:
第一,精度。统计的流量窗口是不是够准确,能不能精确反映一个时间段内的真实请求量。
第二,内存和计算开销。算法的实现是否简单、高效,在高并发下会不会成为新的性能瓶颈。
第三,对突发流量的处理能力。有的场景希望流量完全匀速,有的场景则希望允许一定程度的突发。没有一种算法能同时做到既严格匀速、又允许突发,所以你才能看到这么多种方案存在。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 固定窗口计数器:5分钟能写出来,但临界流量会击穿它
2.1 原理其实就是一个计数器加一个时间窗
固定窗口是所有限流算法里最直观的。我把时间切成一个个固定长度的窗口,比如1秒一个窗口,每个窗口内维护一个计数器。请求进来,计数器加一;计数器超过阈值,请求一律拒绝;进入下一个窗口,计数器清零重新计数。
这种设计和“按秒统计请求数限制为100”是一个意思。实现也非常简单,一个类就搞定。
java复制public class FixedWindowRateLimiter {
private final long maxCount; // 窗口内允许的最大请求数
private final long windowMillis; // 窗口大小,单位毫秒
private long currentWindowStart; // 当前窗口的开始时间
private long currentCount; // 当前窗口内已通过的请求数
public FixedWindowRateLimiter(long maxCount, long windowMillis) {
this.maxCount = maxCount;
this.windowMillis = windowMillis;
this.currentWindowStart = System.currentTimeMillis();
}
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
if (now - currentWindowStart >= windowMillis) {
currentWindowStart = now;
currentCount = 0;
}
if (currentCount >= maxCount) {
return false;
}
currentCount++;
return true;
}
}
这个版本我加了synchronized来保证并发安全。实际生产里如果并发量很高,synchronized会形成锁竞争,你可以换成AtomicLong加时间判断,或者用LongAdder做计数,命中的时候再判断是否超限。不过逻辑都一样,不复杂。
2.2 临界问题:两个窗口交替时,流量直接翻倍
固定窗口最大的问题,是窗口边界前后的流量可以叠加,形成双倍甚至更高的请求尖刺。
假设限流规则是“每秒最多100个请求”,窗口是1秒。某个请求在第1秒的999毫秒时进来,它所在窗口已经积累了100个请求,所以它被拒绝,这没问题。但如果第1秒的999毫秒时,第1个窗口只积累了50个请求;紧接着到第2秒的1毫秒,新窗口开启计数器归零,这时又来了50个请求。表面上每一秒的请求数都是50,没有超过100,但实际上在第1秒的尾巴和第2秒的开头之间,这个系统在极短的时间内连续收到了100个请
