摘要
在微服务架构与分布式系统中,面对突发流量(如电商秒杀、遭遇恶意爬虫或黑客 DDoS 攻击),任何系统的处理能力都是有物理极限的。为了保证核心服务的可用性,避免因雪崩而导致系统全面瘫痪,限流(Rate Limiting)是构建高弹性系统最关键的防御性技术手段。本文将深度剖析四种核心限流算法的数学原理、状态机设计以及在高并发场景下的工程权衡。
一、 简单粗暴的防线:固定窗口算法(Fixed Window)
固定窗口算法是最直观的限流设计。它将时间划分为一个固定的周期(例如 1 分钟),并在内部维护一个计数器。
1. 算法逻辑
-
在当前时间窗口内,每当请求到达,计数器加 1。
-
如果计数器超过了设定的阈值(例如每分钟允许 100 次),则直接拒绝后续的所有请求。
-
当时间推进到下一个周期时,计数器清零,开始重新计数。
2. 致命软肋:窗口临界区"突刺"
固定窗口算法最大的漏洞在于无法处理临界点的双倍并发突刺。
假设限制条件为每分钟 100 次请求。攻击者在 00:59 发送了 100 个请求,全部通过;随后在 01:01 抢在下一个窗口刚开启时,又发送了 100 个请求,同样全部通过。这意味着在 00:59 到 01:01 这短短 2 秒的跨窗口时间内,系统承受了 200 次请求,直接突破了设计带宽的 2 倍,这极易瞬间冲垮底层的数据库或依赖服务的线程池。
二、 细腻的滑动平移:滑动窗口算法(Sliding Window Log / Counter)
为了抹平固定窗口的临界突刺,滑动窗口算法将固定的长周期细分为更小的格子(例如将 1 分钟细分为 6 个 10 秒的格子),并让整个大窗口随着时间持续向前平移。
1. 算法逻辑
-
当一个新请求在
01:15到达时,滑动窗口会计算当前时间点往前推 1 分钟(即00:15 - 01:15)内所有小格子里的请求总数。 -
如果总数未超限,则允许通过,并将当前请求记录在对应的
01:10 - 01:20的格子中。 -
随着时间推移,最老的格子(如
00:00 - 00:10)会被移出视口,从而在物理上保证了任意连续的 1 分钟内的请求密度都绝对处于安全线内。
工程权衡: 滑动窗口极好地规避了突刺问题,但在工程实现中(如基于 Redis ZSet 实现的滑动窗口日志),它需要精确记录每次请求的时间戳并进行范围统计,当并发极高时,其内存占用与计算耗时会显著上升。
三、 绝对匀速的塑形器:漏桶算法(Leaky Bucket)
如果你的下游系统非常脆弱(例如某些老旧的外部第三方支付接口),无论上游流量如何暴增,下游都必须保持绝对稳定的速率,那么漏桶算法是最佳的选择。
漏桶算法的物理模型就像一个底部钻了小孔的无盖水桶:
Plaintext
流量流入 (任意速率, 突发洪峰)
│ │ │
▼ ▼ ▼
┌─────────────┐
│ █████████ │ <--- 桶内积水 (作为缓冲区, 超出则溢出拒绝)
└──────┬──────┘
│
▼ 恒定速率漏出 (匀速交给下游执行)
-
流入:任意突发的流量都可以涌入漏桶中。如果流入速度过快,桶内的"积水"会不断升高;一旦积水超出了桶的最大容量(Buffer Size),多余的流量就会直接溢出(被丢弃或拒绝)。
-
流出 :无论桶里有多少水,底部的漏孔都在以绝对恒定的速度向外滴水,驱动下游业务处理逻辑。
- 优缺点 :漏桶算法强制实行了流量整形(Traffic Shaping) ,彻底消除了解析层面的任何波峰。但由于它要求绝对匀速,这就意味着即使系统当前有很多闲置资源,突发流量也必须在桶里排队等待,无法有效应对正常的突发流量。
四、 拥抱突发的弹性护盾:令牌桶算法(Token Bucket)
为了在"保护系统"与"应对正常的秒杀突发"之间找到平衡,现代开源中间件(如 Google Guava RateLimiter、Sentinel)普遍采用了令牌桶算法。
令牌桶的逻辑与漏桶恰好相反,它是往桶里放凭证:
-
恒定生成:系统以恒定的速率(如每秒生成 R 个)往一个固定容量(Capacity)的桶里放入"令牌(Token)"。如果桶满了,生成的令牌就会被丢弃。
-
动态获取 :当一个请求到达时,它必须先从桶里成功取走一个令牌,才能被允许继续执行;如果桶里没有令牌,说明请求速度过快,该请求会被立刻拦截、降级或阻塞等待。
为什么令牌桶能应对突发流量?
假设令牌桶的最大容量为 100,当前系统长期处于空闲状态,桶里积满了 100 个令牌。 此时,突然涌入 80 个高并发请求,由于桶内有足够的令牌,这 80 个请求可以瞬间同时拿走令牌并并发执行,完美应对了合理的突发洪峰。随后,桶变空了,系统自动退化为按照令牌生成的恒定速率来约束后续请求。
五、 分布式限流的工程挑战:网格下的争用
当系统从单机演进为成百上千个节点的分布式微服务集群时,限流通常会挂载在网关(网关层限流,如 Spring Cloud Gateway、Nginx)或者依赖分布式存储(如 Redis + Lua 脚本):
Lua
-- Redis + Lua 实现令牌桶的伪核心逻辑(保证复合操作的原子性)
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then
return 0 -- 拒绝操作
else
redis.call("INCRBY", key, 1)
redis.call("EXPIRE", key, 1) -- 自动过期滑动
return 1 -- 放行
end
利用 Lua 脚本的原子性,我们可以确保在多台机器同时修改 Redis 中的限流计数器时,不会因为网络交错而引发锁竞争或并发穿透,这也是分布式架构中保证状态一致性的标准解法。
六、 总结
-
固定窗口因其简单而适合粗粒度限流,但无法对抗临界区的两倍并发突刺。
-
滑动窗口通过切分时间片重构了边界的平滑度,但需要消耗更多的内存和计算带宽。
-
漏桶算法是强力的流量整形工具,强制实行绝对匀速交付,适合严苛保护极度脆弱的下游依赖。
-
令牌桶算法凭借其允许短时间突发流量并发通过的弹性特征,成为了目前工业界高并发微服务系统高可用调优的核心底座。