漏斗算法(Funnel Algorithm)
一、核心原理(图示+说明)
+-----------------------+
| |
| 待处理请求 | ← 新请求从顶部注入
| (水量) |
| |
+-----------------------+
| 漏嘴 | ← 按固定速率(leakRate)流出请求
+-----------------------+
↑
超出容量的请求从顶部溢出(被拒绝)
(示意图说明:漏斗上宽下窄,顶部接收请求,底部以固定速率漏水,超出容量的请求从顶部溢出)
-
核心思想:模拟漏斗盛水→漏水的过程,控制请求流量:
- 顶部:接收新请求(像水流注入漏斗)。
- 容量:漏斗最多能装的水量(最大积压请求数)。
- 漏嘴:底部固定大小的出口,以固定速率漏水(单位时间允许通过的请求数)。
- 溢出:当注入速度>漏水速度,水满后多余的水溢出(请求被拒绝)。
二、核心参数(4个关键指标)
| 参数名 | 含义 | 示例值 |
|---|---|---|
capacity |
漏斗总容量(最大可积压请求数) | 100(个) |
leakRate |
漏嘴速率(单位时间允许通过的请求数) | 10(个/秒) |
water |
当前水量(当前积压的请求数) | 30(个) |
lastTime |
上次更新水量的时间戳(秒) | 1620000000 |
三、执行流程(动态调整逻辑),每一次请求都要执行
每次请求到来时,按以下步骤处理(结合图示理解):
-
计算时间差
上次更新时间 →——————— deltaTime ———————→ 当前时间(当前时间 - 上次更新时间 =
deltaTime,即两次请求的间隔) 计算漏水量
这段时间内漏嘴漏掉的水量:
漏水量 = deltaTime × leakRate
(例:间隔3秒,漏嘴速率10个/秒 → 漏水量=3×10=30)更新当前水量
水量随时间减少,且不能为负:
water = max(0, 旧water - 漏水量)
(例:旧water=50,漏水量=30 → 新water=20)-
判断是否允许请求
+-----------------------+ | 当前水量:20 | | | → 新请求到来,判断 20+1 ≤ 100? +-----------------------+ → 是 → 允许(水量变为21) → 否 → 拒绝- 若
water + 1 ≤ capacity:允许请求(water+1,更新lastTime)。 - 否则:拒绝请求(漏斗已满,溢出)。
- 若
四、优缺点与适用场景
| 维度 | 内容 |
|---|---|
| 优点 | 1. 长期流量严格限制在漏嘴速率内; 2. 逻辑简单,易实现。 |
| 缺点 | 1. 高并发下需频繁计算时间差,对性能有要求; 2. 分布式场景需中间件保证一致性。 |
| 适用场景 | 接口限流、秒杀流量控制、用户行为频率限制(如评论、登录)等。 |
五、分布式实现关键点(Redis+Lua)
- 原子性保证:用Lua脚本封装整个流程,避免并发修改导致的水量误差。
- 状态存储:Redis的Hash结构存储4个核心参数(water、lastTime等)。
- 性能优化:连接池复用Redis连接、本地限流前置过滤请求、Redis集群分流。
六、具体的Lua脚本
-- 漏斗限流脚本:key为限流对象(如"user:123"),返回1表示允许,0表示拒绝
local key = KEYS[1]
local capacity = tonumber(ARGV[1]) -- 漏斗容量
local leakRate = tonumber(ARGV[2]) -- 漏嘴速率(请求/秒)
-- 从Redis获取当前漏斗状态(不存在则初始化)
local current = redis.call("HMGET", key, "water", "lastTime")
local water = tonumber(current[1] or 0)
local lastTime = tonumber(current[2] or 0)
local now = tonumber(redis.call("TIME")[1]) -- 当前时间戳(秒)
-- 计算漏水:deltaTime秒内漏掉的水量
local deltaTime = now - lastTime
local leakWater = deltaTime * leakRate
water = math.max(0, water - leakWater) -- 水量不能为负
lastTime = now -- 更新时间戳
-- 判断是否允许新请求
if water + 1 <= capacity then
water = water + 1
redis.call("HMSET", key, "water", water, "lastTime", lastTime)
redis.call("EXPIRE", key, 86400) -- 设置过期时间,避免内存溢出
return 1 -- 允许
else
return 0 -- 拒绝
end