news 2026/5/19 20:32:54

限流的艺术:令牌桶与滑动窗口的博弈,以及我为何在 AI 项目中选择了后者

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
限流的艺术:令牌桶与滑动窗口的博弈,以及我为何在 AI 项目中选择了后者

写在前面

你好,我是 Evan。一名正在摸爬滚打的 Java 后端开发者,也是这个专栏的作者。

今天想和你聊聊一个让我“后知后觉”的话题——限流。说实话,在前两个单体项目和微服务项目中,我几乎没有主动思考过限流。那时候项目 QPS 不过百,写个接口直接上线,根本没遇到被流量打垮的情况。团队里用的限流组件是阿里 Sentinel,配置一下规则就行,底层原理?没深究过。

直到最近做智答 Agent 项目,用户与 AI 对话的模块需要限制每个会话的请求频率(防止恶意刷接口),我才第一次亲手实现了一个滑动窗口计数器。这时我才发现:原来限流算法不是“选一个组件配置”那么简单,它背后是一套关于精度、内存、突发流量的深刻权衡。

这篇文章,我想对比两种最常见的限流算法——令牌桶滑动窗口,并结合我的项目实践,帮你理清:什么时候该用谁,以及为什么 Sentinel 底层用的是滑动窗口。

一、限流的本质:一个“放行”决策问题

限流的任务很简单:判断请求是否应该被处理。但难就难在“标准”如何制定。

最常见的两种策略:

  • 基于时间窗口:比如 1 秒内最多处理 100 次请求。

  • 基于令牌桶:系统以固定速率生成令牌,请求需要取走一个令牌才能被处理。

两者都能实现限流,但它们在应对突发流量统计精度内存开销上差异巨大。

二、滑动窗口:精确到毫秒的“记账本”

2.1 固定窗口的缺陷

最简单的限流算法是固定窗口计数器:将时间切分成固定的窗口(比如 1 秒),每个窗口内计数,超过阈值就拒绝。

窗口0 窗口1 窗口2 [0ms-1000ms] [1000ms-2000ms] ...

致命问题:窗口边界处的流量突刺。
比如限制 1 秒 100 次请求。攻击者可以在第 1 秒的最后 10ms 发起 100 次,在第 2 秒的前 10ms 再发起 100 次,实际 20ms 内就承受了 200 次请求,系统可能被冲垮。

2.2 滑动窗口如何解决问题

滑动窗口将窗口进一步细分(比如 1 秒分成 10 个 100ms 的小格子)。每个格子独立计数,当前窗口的总计数 = 所有未过期的格子之和。

当时间向前滑动时,最旧的格子被丢弃,新的格子加入。

示例:限制 1 秒 100 次。格子大小 100ms,每个格子独立计数。

  • 请求在 0.95s 进入:落在格子9,计数+1。

  • 请求在 1.02s 进入:窗口滑动到 0.2s~1.2s,丢弃格子0,加入新格子,重新计算总和。

这种方式消除了窗口边界突刺,因为窗口是“连续滑动”的,不会出现两个满额窗口背靠背的情况。

2.3 滑动窗口的优缺点

我在智答 Agent 项目中的应用
我们需要限制每个会话(sessionId)每分钟最多请求 AI 接口 30 次。如果用固定窗口,用户可以在第 59 秒和第 61 秒连续发起请求,瞬间 60 次。改用滑动窗口后,窗口大小 60 秒,格子 6 秒(10 个格子),精确控制了请求频率,彻底杜绝了边界突刺。

三、令牌桶:应对突发流量的“水库”

3.1 原理

令牌桶算法有一个“桶”,以固定速率r往桶里放令牌(比如每秒放 10 个)。桶的容量为b(最大突发)。每个请求必须从桶里取走 1 个令牌才能被处理。如果桶空,则拒绝。

3.2 为什么需要令牌桶?

滑动窗口严格控制了“任意时间窗口内的请求总数”,但有时候我们希望允许一定的突发流量。比如:

  • 秒杀刚开始的几毫秒,瞬时流量巨大,但我们希望用户能正常下单,而不是立刻被限流。

  • API 网关对外提供接口,正常速率 1000/s,但偶尔有 2000/s 的突发(持续 1 秒),我们希望放行,而不是一刀切拒绝。

令牌桶允许这样的突发——只要桶里有令牌,请求就可以一次性取走多个令牌,形成短时高峰。然后桶慢慢恢复到稳定速率。

3.3 令牌桶的优缺点

3.4 一个经典例子

假设速率r=10/s,桶容量b=20。系统空闲一段时间后桶满。此时突然涌入 20 个请求,它们都能被放行(突发)。然后接下来的 1 秒内,桶只能以 10/s 的速率补充令牌,所以后续请求最多只能再处理 10 个。

这就是“允许突发,但不会持续超额”。

四、一张表看懂:滑动窗口 vs 令牌桶

实际选择

  • 如果你的需求是“绝对不能让用户在一段时间内超过 N 次”,用滑动窗口(如防刷、限流验证码)。

  • 如果你的需求是“保护系统不被长时间高流量压垮,但允许短时峰刺”,用令牌桶(如网关限流、数据库连接池限流)。

五、从实践到原理:Sentinel 为什么选择滑动窗口?

阿里 Sentinel 是目前 Java 生态最流行的限流组件。它的底层实现就是滑动窗口,而不是令牌桶。为什么?

  1. 精确控制:Sentinel 需要支持多种限流规则(QPS、线程数、关联资源等),滑动窗口能提供精确的统计值。

  2. 无需预测:令牌桶需要预估“未来令牌数”,而 Sentinel 更看重“过去实际请求数”。

  3. 易于降级:滑动窗口可以直接根据实时统计触发熔断,更贴近“响应式”设计。

但 Sentinel 也提供了匀速排队模式(类似令牌桶),用于处理突发。所以它不是完全抛弃令牌桶思想,而是根据场景选择。

我的项目经验
在智答 Agent 中,AI 对话模块单次调用耗时长(2~5 秒),且涉及计费。不允许任何突发,必须严格限制频率。所以我选了滑动窗口,直接基于 Redis 的ZSET实现(按时间戳存储请求记录,清理过期条目)。如果用令牌桶,突发放行可能导致 AI 服务短时过载,不可接受。

六、你在项目中何时该考虑限流?

即使你的项目 QPS 很低,也应该提前考虑限流,原因有三:

  1. 防止恶意攻击:公开接口容易被刷(短信验证码、搜索接口)。

  2. 保护下游依赖:你的服务可能调用数据库、消息队列或第三方 API,它们都有承载上限。

  3. 公平性:多租户场景下,限制某个用户的调用次数,防止资源被占满。

最小实践
在 Spring Boot 项目中,你可以用@RateLimiter注解 + AOP 快速实现一个简单的滑动窗口(基于 Guava Cache 或 Redis)。先让限流存在,再慢慢优化。

@RateLimiter(limit = 100, window = 60) // 60秒内最多100次 public Result askAI(String question) { ... }

思考:在你的项目中,有一个对外提供的 API 接口,正常 QPS 大约 500。但每隔几分钟会有一次 1 秒内 2000 QPS 的突发流量(来自合法用户的行为聚集)。如果使用滑动窗口限制 1000 QPS,会导致部分合法请求被误拦;如果使用令牌桶并设置桶容量 2000,又可能导致持续高流量压垮系统。你有什么优化的思路?欢迎在评论区分享你的方案。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/19 20:30:53

国密SM2实战:一份代码搞定JS、C#、Java的加密解密互通(附避坑指南)

国密SM2实战:跨语言加密解密的终极解决方案 在微服务架构盛行的今天,前后端分离、多语言并存已成为技术团队的常态。当JavaScript前端需要与C#、Java后端协同工作时,加密解密的一致性往往成为联调过程中的"暗礁"。我曾在一个金融项…

作者头像 李华
网站建设 2026/5/19 20:26:37

Claude Code 用户如何通过 Taotoken 配置稳定 API 连接避免封号困扰

🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度 Claude Code 用户如何通过 Taotoken 配置稳定 API 连接避免封号困扰 基础教程类,针对经常遇到 Claude Code 封号或 Tok…

作者头像 李华
网站建设 2026/5/19 20:22:06

AArch64 TRCIDR寄存器详解与调试实践

1. AArch64寄存器系统概述 AArch64是Armv8及后续版本架构中引入的64位执行状态,它为现代计算设备提供了强大的处理能力和高效的指令集。在Arm C1-Premium Core这样的高性能处理器中,寄存器系统扮演着核心角色,不仅是数据处理的基础&#xff0…

作者头像 李华