Java 限流算法实现:令牌桶 / 漏桶 / 滑动窗口

aixiu
aixiu 正式会员正式会员认证极客认证极客
发布于 2026-10-08 00:46 ·2 浏览 ·0 回复

学完这篇你能拿到三份可直接抄进项目的 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。

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-746.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~