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

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 |

相关推荐
starzy19904 小时前
深入理解Scala隐式类:扩展方法机制
开发语言·人工智能·scala
Pniubi4 小时前
怎么排查慢SQL,怎么解决?
java·数据库·sql
M--Y4 小时前
LangGraph 工作流模式
开发语言·javascript·python·langchain·大模型·llm·langgraph
薛之谦_5 小时前
Java 全栈知识体系:从 IDEA 高效开发到 JVM、并发与 Stream 实战
java·jvm·intellij-idea
forest_095 小时前
多 Agent 协作第二课:交叉验证≠独立验证
人工智能·python·算法·机器学习·prompt·agi
吴可可1235 小时前
WScript Shell操作注册表详解
开发语言
纪念 2296 小时前
C++ 算法基础 (差分)
开发语言·c++·算法
文丑颜不良啊9 小时前
LeetCode2761~2780题目解析
java·leetcode
jyOverQ9 小时前
从零学 JVM(六):垃圾回收算法详解——标记清除、标记复制与标记整理
jvm·算法
IT枫斗者枫哥9 小时前
子线程断言失败,JUnit 为什么还显示通过?
java·后端