Java 限流算法实现:令牌桶 / 漏桶 / 滑动窗口
学完这篇你能拿到三份可直接抄进项目的 Java 限流器代码,并搞清令牌桶、漏桶、滑动窗口各自适合什么场景。
限流器的核心只有一件事:给定一个阈值,判断「这次请求放行还是拒绝」。所以先定一个统一接口,三个实现都挂上去,调用方不用关心内部算法。
第一步:定一个统一的限流器接口
public interface RateLimiter {
/** 非阻塞,拿不到配额立刻返回 false */
boolean tryAcquire();
/** 阻塞等待,最多等 timeoutMs 毫秒 */
default boolean acquire(long timeoutMs) throws InterruptedException {
long deadline = System.currentTimeMillis() + timeoutMs;
while (System.currentTimeMillis() < deadline) {
if (tryAcquire()) return true;
Thread.sleep(10);
}
return tryAcquire();
}
}
注意:生产环境优先用
tryAcquire(),配合快速失败返回「系统繁忙,请稍后再试」。acquire()阻塞版本容易在流量洪峰时把线程池占满,反而拖垮服务。
第二步:令牌桶(允许突发流量)
思路:桶里最多存 capacity 个令牌,按固定速率往桶里补,请求来了先取令牌,取不到就拒绝。补令牌用「惰性计算」,不启定时线程。
public class TokenBucketLimiter implements RateLimiter {
private final long capacity; // 桶容量,决定能扛多大的突发
private final double ratePerNano; // 每秒生成多少令牌
private double tokens;
private long lastRefillNanos;
public TokenBucketLimiter(long capacity, double permitsPerSecond) {
if (capacity <= 0 || permitsPerSecond <= 0) {
throw new IllegalArgumentException("capacity 和速率必须大于 0");
}
this.capacity = capacity;
this.ratePerNano = permitsPerSecond / 1_000_000_000d;
this.tokens = capacity; // 启动时桶是满的
this.lastRefillNanos = System.nanoTime();
}
@Override
public synchronized boolean tryAcquire() {
long now = System.nanoTime();
tokens = Math.min(capacity, tokens + (now - lastRefillNanos) * ratePerNano);
lastRefillNanos = now;
if (tokens >= 1) {
tokens -= 1;
return true;
}
return false;
}
}
关键参数就两个:capacity 决定突发上限,permitsPerSecond 决定长期平均速率。比如 new TokenBucketLimiter(100, 20) 表示长期每秒 20 个,但空闲后最多能一次性放行 100 个。
注意:
System.nanoTime()只保证单机单调递增,不能用它在多台机器间做时间基准。另外synchronized包住整个方法在高并发下会成为瓶颈,QPS 上万的接口建议改成 CAS 自旋或按线程 ID 分片成多个桶。
第三步:漏桶(恒定速率输出,不放过突发)
漏桶的水以固定速率漏出,桶满了新请求直接丢弃。实现上不需要真的存水,用「下一次可放行的时间戳」即可。
public class LeakyBucketLimiter implements RateLimiter {
private final long capacity; // 最多积压多少个请求
private final long intervalNanos; // 每滴水的间隔
private long nextFreeNanos;
public LeakyBucketLimiter(long capacity, double permitsPerSecond) {
this.capacity = capacity;
this.intervalNanos = (long) (1_000_000_000d / permitsPerSecond);
this.nextFreeNanos = System.nanoTime();
}
@Override
public synchronized boolean tryAcquire() {
long now = System.nanoTime();
if (nextFreeNanos < now) {
nextFreeNanos = now; // 桶空了,时间基准对齐到现在
}
if (nextFreeNanos - now > capacity * intervalNanos) {
return false; // 桶已满
}
nextFreeNanos += intervalNanos; // 预定下一个放行时刻
return true;
}
}
注意:令牌桶和漏桶最本质的区别是「要不要突发」。令牌桶空闲时会攒令牌,来一波洪峰能扛住;漏桶输出永远平滑,适合给下游脆弱系统(第三方 API、老数据库)做保护。选错了会出现「明明没超限却一直被拒」或「突发打穿下游」。
第四步:滑动窗口(按时间片计数,最贴近「最近 1 秒 N 次」)
固定窗口会有临界问题:1 秒末尾放 100 个、下一秒开头放 100 个,实际 200ms 内过了 200 个。滑动窗口把时间切成小格,每次请求统计「最近一整窗」的格子和。
public class SlidingWindowLimiter implements RateLimiter {
private final int limit; // 窗口内允许的请求数
private final int slotCount; // 切成几格,例如 10
private final long slotMillis; // 每格毫秒数
private final int[] counters;
private int currentSlot;
private long currentSlotStartMillis;
public SlidingWindowLimiter(int limit, int windowSeconds, int slotCount) {
this.limit = limit;
this.slotCount = slotCount;
this.slotMillis = windowSeconds * 1000L / slotCount;
this.counters = new int[slotCount];
this.currentSlotStartMillis = System.currentTimeMillis();
}
@Override
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
long steps = (now - currentSlotStartMillis) / slotMillis;
if (steps > 0) {
int clear = (int) Math.min(steps, slotCount);
for (int i = 1; i <= clear; i++) {
counters[(currentSlot + i) % slotCount] = 0; // 过期格子归零
}
currentSlot = (int) ((currentSlot + steps) % slotCount);
currentSlotStartMillis += steps * slotMillis;
}
int total = 0;
for (int c : counters) total += c;
if (total >= limit) return false;
counters[currentSlot]++;
return true;
}
}
调用示例:new SlidingWindowLimiter(100, 1, 10) 表示 1 秒最多 100 次,内部按 100ms 一格统计。
注意:
slotCount越大精度越高,但每次请求都要遍历数组求和,内存和 CPU 成本同步上升。一般取 10~20 格就够了,别设成 1000。
转载请注明出处,版权归原作者所有。
正式会员
认证极客






