在分布式系统中,流量防护是一道绕不开的课题。Redis凭借高性能与原子操作特性,成为实现限流组件的常用选择。所谓漏斗限流,本质上是漏桶算法的工程落地:请求像水一样流入桶中,桶以固定速率漏水,一旦流入速度超过漏出速度且桶满,多余请求就被拒绝。这种方式能把任意突发的请求速率整形为平稳的输出速率,从而保护后端服务。
一、为什么普通计数限流不够精确
很多团队最初会用Redis的INCR配合过期时间做固定窗口限流,例如每秒允许100次请求。这种做法在单窗口内看似有效,却在窗口边界存在严重漏洞。假设限制每秒100次,第0.9秒涌入100次,第1.1秒又涌入100次,实际两秒内系统承受了200次,但按每秒口径均未超标。这种临界突变会让后端在瞬间遭遇两倍负载。
滑动窗口可以缓解这个问题,但依然难以做到真正的速率整形。漏桶算法的不同之处在于,它不关心统计周期,只保证长期平均处理速率恒定。只要桶容量设得合理,系统在任何短暂区间内都不会被压垮。因此,当业务要求精确控制单位时间处理能力时,漏斗模型比简单计数更可靠。
二、Redis漏斗的核心数据结构
实现漏斗限流通常借助Redis的Sorted Set(有序集合)或Hash。以有序集合为例,每个请求到达时,用当前时间戳作为score写入一个member,表示一滴水进入桶中。同时,启动一个逻辑上的漏水过程:删除score小于(当前时间减桶容量对应时间窗)的成员,剩下的数量就是桶内余水。若余水小于桶容量则放行,否则拒绝。
另一种更轻量的做法是使用Hash记录上次漏水时间与剩余水量,每次请求时先按流逝时间算出应漏掉多少,再判断是否可加入。这种方案只需一次HGET和HSET,性能更高,也更容易理解。无论哪种结构,都必须用Lua脚本保证读写原子性,否则并发场景下计数会错乱。
2.1 Lua脚本保证原子性
Redis执行Lua脚本时是单线程原子操作,这正好契合漏斗计算的需求。脚本内部先读取余量与时间戳,计算漏水后若未满则更新并返回允许,否则返回拒绝。由于脚本在服务器端一次性跑完,不受客户端网络延迟干扰,精度远高于先GET再逻辑判断再SET的方式。
下面给出一个简化的脚本逻辑描述:传入容量、速率、当前时间;算出距离上次漏水经过的秒数,乘速率得漏掉量,余量减漏掉量最低为0;若余量加1不大于容量,则余量加1并保存时间,返回1;否则保存原值返回0。业务端根据返回值决定放行或抛限流异常。
三、关键参数与精度调优
漏斗有两个核心参数:容量(capacity)与速率(rate)。容量决定瞬时容忍上限,速率决定长期吞吐。若容量过大,突发流量虽不被拒却会堆积;过小则正常抖动也被误杀。一般建议容量设为速率的1到3倍,例如每秒处理50请求,容量给100到150之间。
精度方面,时间单位越细越好。用毫秒级时间戳能降低因秒级取整带来的误差,但也会让有序集合成员更多。若采用Hash余量法,毫秒计算几乎无额外成本,应优先使用。此外,Redis集群下需保证同一限流维度落在同一槽位,否则脚本跨节点会失败,可通过哈希标签如{user123}来固定。
| 参数 | 含义 | 设置建议 |
|---|---|---|
| capacity | 桶最大水量 | 速率的1到3倍 |
| rate | 每秒漏水速率 | 等于后端安全吞吐 |
| time_unit | 计算时间精度 | 优先毫秒 |
四、常见误差与应对
实际部署中,时钟漂移可能让多台应用服务器时间不一致,若把本地时间传给Redis算漏水,会出现水量突增或拒流。解决办法是所有时间以Redis服务器时间为准,在脚本内用TIME命令获取,或统一由网关层打时间戳。
另一个误区是认为漏斗能完全防止过载。当速率设置高于后端真实处理能力,桶再大也会持续满溢并导致请求在应用层排队。限流值必须基于压测得到的真实容量,并预留缓冲。配合降级与熔断,才能构建完整防护链。
五、落地场景示例
某开放API平台对每个AppKey限制每秒20次调用。使用Redis漏斗后,即便客户端在月初脚本批量重跑时瞬间发出上千请求,也只有前20个左右进入首秒,其余按恒定速率通过或被拒。后端实例数量从十台降到三台仍平稳,成本明显下降。
在秒杀系统中,漏斗还可作为第一道闸门,将抢购请求匀速送进库存扣减逻辑,避免超卖与数据库死锁。实践中把容量设低、速率按预估人流量给,能最大限度平衡公平与性能。