Redis实现限流的三种核心方案详解
在分布式系统的高并发场景下,限流是守护服务稳定性的最后一道防线。当突发流量、恶意爬虫或者接口刷量请求涌入时,没有限流保护的后端服务很容易在短时间内被打垮,出现数据库连接池耗尽、CPU占用率飙升、核心业务不可用等严重故障。而Redis凭借其毫秒级的读写性能、丰富的数据结构和天然的分布式特性,成为了业界实现限流方案的首选载体。不同的限流方案适配不同的业务场景,从最简单的固定窗口计数器,到兼顾精度与性能的滑动窗口,再到平滑流量的令牌桶,三种主流方案各有优劣,吃透它们的实现原理、适用边界和优化技巧,才能在实际项目中选出最适配业务需求的限流策略。
固定窗口计数器限流:最简单高效的入门级方案
固定窗口限流的核心逻辑非常直观:把时间轴按照固定的粒度切分成一个个独立的时间窗口,比如1秒、1分钟或者1小时,在每个独立的窗口内统计用户或接口的请求次数,一旦请求数超过预先设定的阈值,就直接拒绝后续请求,直到下一个新的窗口重新开始计数。
它的实现逻辑几乎没有任何复杂度:我们用Redis的String类型做计数器,每次收到请求时调用INCR命令对指定的限流键做自增操作。如果自增后的结果刚好等于1,说明这是当前窗口的第一个请求,我们就给这个键设置一个和窗口时长相等的过期时间。最后只需要判断自增后的数值是否小于等于预设的阈值,就能决定是否放行当前请求。整个实现只需要几行核心代码,没有任何复杂的逻辑,性能极高,哪怕是每秒上万次的请求,Redis也能轻松承载。
这种方案的优势在于资源消耗极低,不需要维护额外的复杂数据结构,部署和调试成本几乎为零,非常适合业务流量分布均匀、对限流精度要求不高的场景。比如小型网站的普通接口限流、后台管理系统的登录请求频率控制,这类场景下固定窗口限流完全可以满足需求,不会出现明显的问题。
但它的致命缺陷是存在经典的“临界突增问题”:假设我们设置1分钟内最多允许100次请求,在第一个窗口的最后1秒,用户发起了100次请求,紧接着下一个窗口的第1秒又发起了100次请求,相当于在短短2秒的时间内系统收到了200次请求,远超每分钟100次的限流阈值。这种边界处的流量突增,很容易把没有做好冗余保护的服务直接打垮,在高并发的核心业务场景下,这个缺陷是完全不可接受的。
滑动窗口限流:解决边界突增的高精度方案
为了弥补固定窗口的临界突增缺陷,滑动窗口限流应运而生。它不再把时间切割成一个个互不重叠的固定窗口,而是维护一个持续向前滑动的动态时间窗口,比如“最近60秒”这个不断移动的时间区间,只要落在这个区间内的请求都会被统计,从根源上避免了窗口切换时的流量漏洞。
滑动窗口的标准实现依赖Redis的有序集合ZSet数据结构:我们把每个请求的唯一标识(可以直接用当前请求的时间戳作为唯一值)作为ZSet的元素,同时把当前请求的时间戳作为元素的score值。每次收到新请求时,首先执行ZREMRANGEBYSCORE命令,把ZSet里所有score值小于“当前时间戳减去窗口时长”的过期元素全部删掉,清理掉窗口之外的历史请求记录。之后统计当前ZSet里剩余的元素总数量,如果数量小于限流阈值,就把当前请求的时间戳作为新元素插入ZSet,直接放行请求;如果数量已经超过阈值,就直接拒绝当前请求。
这种方案的限流精度极高,完全不存在固定窗口的边界突增问题,能精准控制任意连续时间区间内的请求总数,非常适合对流量稳定性要求高的核心业务场景。比如电商的商品下单接口、支付系统的回调接口,这类场景下绝对不允许短时间内出现流量突增,滑动窗口限流可以完美满足需求。
当然它也有自己的短板:如果限流窗口很大、阈值很高,ZSet里需要存储大量的请求记录,会占用较多的Redis内存资源。不过在实际业务中,绝大多数场景的限流窗口都不会超过几分钟,配合定期清理过期元素的操作,内存消耗完全在可控范围内。现在很多网关级别的限流组件,比如OpenResty的限流模块,底层都是基于滑动窗口的逻辑实现的,在互联网大厂的高并发系统里已经得到了非常广泛的验证。
令牌桶限流:实现流量平滑的工业级方案
前面两种限流方案都是“控制请求总数”,但在很多真实场景下,业务不仅要求限制总请求数,还希望流量的到来是平滑均匀的,避免出现大量请求在同一时刻集中涌入的“流量毛刺”,这时候令牌桶限流就是最优选择。
令牌桶的核心逻辑非常巧妙:我们以一个固定的速率往桶里放入令牌,比如每秒生成10个令牌,桶本身有一个最大容量。每个请求想要被放行,必须先从桶里拿到一个可用的令牌,如果桶里已经没有令牌了,就直接拒绝请求。这种机制天然就实现了两个核心特性:一方面长期来看,请求的平均速率不会超过令牌生成的速率,满足限流的要求;另一方面桶可以积累一定数量的令牌,允许短时间的突发流量通过,兼顾了系统的弹性和稳定性。
基于Redis的令牌桶实现,我们通常用Hash结构来存储桶的两个核心状态:桶里当前剩余的令牌数量,以及上一次补充令牌的时间戳。每次收到请求时,首先根据当前时间戳和上一次补充令牌的时间差,计算出这段时间里应该新生成的令牌数量,把令牌数补充到桶里,注意不能超过桶的最大容量。之后判断剩余令牌数是否大于等于1,如果是就消耗一个令牌,放行当前请求,更新剩余令牌数和时间戳;如果剩余令牌数为0,就拒绝请求。
令牌桶的优势在于它能把不规则的突发流量,整形为相对平滑的流量输出,非常适合网关层的全局限流场景,比如API网关的出口流量控制、微服务之间的调用频率管控。它既可以限制长期的平均调用速率,又不会完全拒绝合理的突发请求,是现在分布式系统网关限流的主流工业级方案。
三种基于Redis的限流方案,没有绝对的优劣之分,只有适配场景的区别。固定窗口适合简单轻量的低并发场景,滑动窗口适合高精度的核心业务,令牌桶适合需要流量整形的网关层场景。在实际生产环境中,我们还可以结合Lua脚本把整个限流逻辑封装成原子操作,避免并发场景下的竞态问题,再配合Redis集群实现水平扩展,完全可以支撑起十万甚至百万级QPS的高并发限流需求,为整个分布式系统构建起一道坚固的流量防护墙。





