三大主流限流算法简介
这三种算法是分布式系统中实现限流的基石。它们都借助 Redis 的高性能内存读写和 Lua 脚本的原子性操作,来确保在多台服务器(集群)环境下限流策略的准确性和一致性。
1. 令牌桶算法 (Token Bucket)
这是目前应用最广泛的算法,因为它在限制平均速率的同时,允许一定程度的突发流量。
核心思想 (比喻)
想象一个会自动出票的机器(桶),它以固定的速度(比如每秒2张)出票,但最多只能存放10张票(容量)。每个请求过来,都必须从机器里拿走一张票才能通过。- 匀速出票:系统按固定速率生成令牌。
- 允许突发:如果短时间内没有请求,票会积攒起来。当突然来了一批请求时,它们可以快速消耗掉所有积攒的票,从而被立即处理。
- 速率限制:一旦积攒的票用完,请求的处理速度就只能等于票的生成速度。
Redis 实现方式
- 数据结构:
HASH
。 - 字段:
tokens
: 当前剩余的令牌数。last_ts
: 上次刷新令牌的时间戳。
- Lua 脚本逻辑:
- 根据当前时间与
last_ts
的时间差,计算应补充多少新令牌。 - 将新令牌加入桶中(不能超过容量)。
- 判断当前令牌数是否足够本次请求消耗(通常是1)。
- 如果足够,则扣减令牌,更新
last_ts
,返回成功。否则返回失败。
- 根据当前时间与
- 数据结构:
特点与适用场景
- 特点: 灵活,既能限制平均速率,又能应对突发流量。
- 场景: 通用的 API 接口限流、网关限流、电商秒杀等,适用于大部分需要限流的Web服务。
2. 漏桶算法 (Leaky Bucket)
漏桶算法的核心是强制平滑流出速率,非常适合保护下游系统。
核心思想 (比喻)
想象一个底部有洞的木桶。水(请求)可以随时、快速地倒入桶中,但水只能以固定的速度从洞口漏出(被处理)。- 请求积压:请求到来时,如果桶里还有空间,就先存放在桶里。
- 匀速处理:系统以恒定的速率从桶里取出请求来处理,不管桶里积压了多少。
- 拒绝请求:如果水倒得太快,桶满了,多余的水就会溢出(请求被直接拒绝)。
Redis 实现方式
- 数据结构:
HASH
。 - 字段:
level
: 桶的当前水位(积压的请求数)。last_ts
: 上次漏水的时间戳。
- Lua 脚本逻辑:
- 根据当前时间与
last_ts
的时间差和漏出速率,计算漏掉了多少水(处理了多少请求)。 - 更新当前水位。
- 判断加入新请求后,水位是否会超过桶的容量。
- 如果不会溢出,则增加水位,更新
last_ts
,返回成功。否则返回失败。
- 根据当前时间与
- 数据结构:
特点与适用场景
- 特点: 强制平滑处理速率,有效地保护后端服务。
- 场景: 短信发送、邮件推送、调用第三方有严格频率限制的API等,确保不会因突发流量冲垮下游系统。
3. 滑动窗口算法 (Sliding Window)
这是一种非常精确的限流算法,完美解决了简单计数器在窗口边界的统计不准问题。
核心思想 (比喻)
想象你在看一列匀速行驶的火车,你的眼睛只能透过一个固定长度的“窗口”去看。你关心的是,在任何时刻,你窗口里能看到的火车车厢(请求)有几节。- 动态窗口:这个窗口随着时间的推移不断向前“滑动”。
- 精确计数:只统计当前时间窗口内的请求总数。
- 清理过期记录:随着窗口滑动,旧的、已经滑出窗口的请求记录会被自动清除。
Redis 实现方式
- 数据结构:
ZSET
(有序集合)。 - 存储内容:
Score
: 请求的毫秒级时间戳。Member
: 唯一的请求ID(通常是时间戳-UUID
),以防重复。
- Lua 脚本逻辑:
- 使用
ZREMRANGEBYSCORE
命令,原子性地移除所有时间戳早于“当前时间 - 窗口大小”的旧记录。 - 使用
ZCARD
命令,获取当前窗口内还剩下多少记录。 - 判断该数量是否小于限流阈值。
- 如果小于,则用
ZADD
将当前请求加入ZSET
,返回成功。否则返回失败。
- 使用
- 数据结构:
特点与适用场景
- 特点: 限流控制非常精确,平滑度高。
- 场景: 对限流精度要求极高的场景,如金融交易、防爬虫、防止密码暴力破解等。
总结对比
算法 | 核心比喻 | 关键特点 | Redis 数据结构 | 典型场景 |
---|---|---|---|---|
令牌桶 | 存票消费 | 允许突发流量 | HASH |
通用API限流、秒杀 |
漏桶 | 匀速漏水 | 强制平滑速率 | HASH |
保护下游服务、推送 |
滑动窗口 | 移动时间窗 | 精确控制 | ZSET |
金融、防刷、防爆破 |
源码实现
使用
- 引入依赖
1 | <dependency> |
- 环境配置
1 | spring: |
- 使用