限流算法——固定窗口、滑动窗口、漏桶、令牌桶

1. 计数器算法(固定窗口 → 滑动窗口)

把时间切成固定长度的格子,比如每 1 秒一格,每格维护一个计数器。请求进来就加一,超过阈值就拒绝,进入下一格时清零。

java 复制代码
long windowStart; int count; int limit = 100;
synchronized boolean tryAcquire() {
    long now = System.currentTimeMillis();
    if (now - windowStart >= 1000) { windowStart = now; count = 0; }
    return ++count <= limit;
}

致命缺陷是临界突刺。 假设限流每秒 100 个,有人在 0.9s 到 1.0s 之间打进 100 个,又在 1.0s 到 1.1s 之间打进 100 个。两个窗口各自都没超限,但在这 0.2 秒里系统实际扛了 200 个请求,是限额的 2 倍。这样就会导致限流实际上没有限住

滑动窗口就是为了修这个问题。它把 1 秒再切成 N 个小格(比如 10 个 100ms),统计时只看"当前时刻往前推 1 秒"覆盖的那些小格之和。窗口随时间一格一格往前滑,临界突刺被大大削弱。小格切得越细越精确,代价是内存和计算更多。极端情况下可以记录每个请求的时间戳(滑动日志),这样最精确,但也最费内存。

计数器类算法的本质:只管总量,不管请求在时间上的分布。

2. 漏桶算法(Leaky Bucket)

一个底部有小孔的桶:请求像水一样从上面倒进来,桶底以恒定速率往外漏,漏出去的请求才被处理。桶满了,新来的水就溢出,也就是请求被拒绝。

它的核心特点是:不管输入多猛,输出永远是匀速的。它是一个流量整形器,把突发流量"削峰填谷"成平滑流量。

java 复制代码
long lastLeak; double water; int capacity = 100; double rate = 10; // 每秒漏10个
synchronized boolean tryAcquire() {
    long now = System.currentTimeMillis();
    water = Math.max(0, water - (now - lastLeak) / 1000.0 * rate); // 先算漏掉了多少
    lastLeak = now;
    if (water + 1 <= capacity) { water++; return true; }
    return false;
}

优点 :保护下游效果最好,适合下游处理能力固定、经不起任何抖动的场景,比如调用第三方接口、写数据库。

缺点:太死板。即使系统此刻很空闲,突发请求也只能排队慢慢漏,不能利用空闲资源快速处理,响应延迟会变大。

3. 令牌桶算法(Token Bucket)

反过来想:系统以恒定速率往桶里放令牌 ,桶有容量上限,满了就不再放。请求来了先去桶里拿一个令牌,拿到才能被处理,拿不到就被拒绝或等待。

与漏桶的关键区别在于:空闲时令牌会攒起来 。突发流量到来时,可以一次性消耗掉攒下的全部令牌,瞬间放行一批请求。所以令牌桶限制的是平均速率,同时允许一定程度的突发,突发上限就是桶的容量。

java 复制代码
long lastRefill; double tokens; int capacity = 100; double rate = 10; // 每秒放10个
synchronized boolean tryAcquire() {
    long now = System.currentTimeMillis();
    tokens = Math.min(capacity, tokens + (now - lastRefill) / 1000.0 * rate); // 懒加载补令牌
    lastRefill = now;
    if (tokens >= 1) { tokens--; return true; }
    return false;
}

注意一个实现细节:真实代码不会开一个定时线程去放令牌,而是在每次请求时根据时间差懒计算应该补多少。 上面的漏桶也是同样的思路。Guava 的 RateLimiter 就是令牌桶,还额外支持预热(SmoothWarmingUp),让系统冷启动时逐步放量。

漏桶 vs 令牌桶

|--------|------------------|--------------------------|
| | 漏桶 | 令牌桶 |
| 控制的对象 | 流出速率 | 流入速率 |
| 突发流量 | 不允许,强制匀速 | 允许,最多突发"桶容量"个 |
| 空闲后的表现 | 和平时一样慢 | 能一口气处理一批 |
| 视角 | 保护下游(我不能被压垮) | 保护自己,同时尽量多干活 |
| 典型应用 | 调用外部接口、消息匀速消费 | API 网关、Guava RateLimiter |

相关推荐
怕浪猫1 小时前
Claude Code vs Codex:Computer Use 实现原理深度拆解
算法·面试·github
喵了几个咪2 小时前
GoWind CMS 拆解:在 Go 里给 TipTap 搭一套 Yjs 协同后端
开发语言·后端·golang
程序员清风2 小时前
数据处理脚本工程化:日志、配置、命令行与测试
服务器·开发语言·数据库
文丑颜不良啊3 小时前
LeetCode876~900题目解析
java·leetcode
罗西的思考3 小时前
AI 时代的 “房谋杜断”:如何培养能力、品味与判断力
人工智能·算法·机器学习
aloha_7893 小时前
Java 基础、集合、异常与 JUnit 5 速查
java·junit·sqlserver
淡海水3 小时前
07-02-源码-.NET Core GC分配路径
算法·unity·c#·.netcore·gc·1024程序员节
知守观4 小时前
设计模式实战:从真实老项目中看标准模板方法与工厂的落地
java·spring·设计模式
2601_962885724 小时前
如何用 Python 把 K 线重采样成任意周期?
开发语言·python