Redis滑动窗口限流算法怎么实现?原理与代码详解

来源:C++教程作者:吴凌云头衔:网络博主
导读:本期聚焦于吴凌云创作的《Redis滑动窗口限流算法怎么实现?原理与代码详解》,敬请观看详情。限流是保护系统稳定性的重要手段,而固定窗口算法存在临界突刺问题,在窗口切换瞬间可能放过双倍流量。滑动窗口算法通过将时间窗口切分为多个小格子,让统计随着时间平滑移动,有效解决了这一缺陷。本文从固定窗口的缺陷讲起,深入剖析滑动窗口的底层原理,给出基于Redis有序集合的实现方案,并用Lua脚本保证原子性操作。文中还对比了漏桶与令牌桶算法的差异,分析各方案的性能与适用场景,最后提供分布式部署下的落地建议,帮助你选对限流策略,避免线上服务被流量打垮。

接口限流是高并发系统的第一道防线。假设你的订单接口每秒最多允许100次访问,一旦瞬时流量超过系统承载能力,数据库连接池被打满、服务雪崩就是分分钟的事。常见的限流算法有固定窗口、滑动窗口、漏桶、令牌桶几种,其中滑动窗口因为实现简单、精度够用,配合Redis可以轻松支撑分布式环境,成为很多团队的首选方案。这篇文章就来把Redis实现滑动窗口限流的原理和代码完整讲清楚。

Redis滑动窗口限流算法怎么实现?原理与代码详解

为什么固定窗口有限流临界问题

先看最朴素的固定窗口算法:把时间按固定长度切分,比如每1秒一个窗口,用INCR命令给当前窗口计数,超过阈值就拒绝请求。实现上只需要一个key,每个窗口对应一个计数器,可以用EXPIRE设置过期时间自动清理。

这个方案的问题在于窗口边界。假设限流阈值是每秒100次,如果用户在第0.9秒到1.0秒之间打了99次请求,又在1.0秒到1.1秒之间打了99次请求,两个窗口各自都没超限,但实际上在0.9秒到1.1秒这200毫秒内,系统承受了198次请求。这就是所谓的临界突刺问题,流量在窗口切换的瞬间可能出现双倍峰值,对于脆弱的下游服务来说这可能就是压垮骆驼的最后一根稻草。

滑动窗口的思路就是解决这个问题的:不再把窗口看作一个固定的时间段,而是让窗口的起点跟着当前时间走。任意时刻往回看1秒,这1秒内的请求数量才是判断依据。这样无论流量怎么分布,任意连续1秒内的请求数都不会超过阈值,突刺问题自然就消失了。

滑动窗口的两种实现思路

第一种是滑动窗口日志法。用Redis的有序集合(Sorted Set)记录每个请求的时间戳,成员可以是唯一ID或时间戳加随机数,分值就是请求发生的毫秒时间。判断限流时,先删掉窗口之外的老数据,再统计集合中剩余的成员数量,数量超过阈值就拒绝,否则把当前请求加入集合并放行。这种方式精度最高,精确到每一个请求,但存储量和请求量成正比,高QPS场景下内存占用需要留意。

第二种是滑动窗口计数法,也就是把大窗口切成多个小格子。比如1秒的窗口切成10个100毫秒的格子,每个格子维护一个计数器,判断时把最近10个格子的计数加起来和阈值比较。这种方式内存占用固定,但精度受格子粒度限制,本质上是对滑动窗口的近似。Sentinel等框架内部就是采用的这种格子思想。下面主要讲解基于有序集合的日志法实现,它更直观也更常用。

用Redis有序集合实现滑动窗口限流

核心数据结构选ZSET,分值存储请求的毫秒时间戳,成员需要保证唯一性,否则同一毫秒内的多个请求会互相覆盖导致计数偏小。常见做法是用时间戳加随机数,或者用请求的唯一标识。整理一下完整的判断流程:

  • 记录当前时间戳now,计算窗口起点now - windowSize
  • ZREMRANGEBYSCORE移除分值小于窗口起点的所有旧记录
  • ZCARD统计集合内剩余的请求数量
  • 如果数量大于等于阈值,返回拒绝;否则用ZADD添加当前请求,并用PEXPIRE给key设置一个略大于窗口的过期时间防止冷key堆积

这几步操作必须保证原子性。如果在应用层分多条命令执行,两个并发请求可能同时读到相同的计数,双双通过校验,导致限流形同虚设。解决办法就是把全部逻辑塞进Lua脚本,Redis执行Lua脚本是原子的,执行期间不会有其他命令插队。脚本如下:

-- KEYS[1]: 限流key
-- ARGV[1]: 窗口大小(毫秒)
-- ARGV[2]: 最大请求数
-- ARGV[3]: 当前时间戳(毫秒)
-- ARGV[4]: 唯一成员ID

local key = KEYS[1]
local window = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local member = ARGV[4]

-- 1. 移除窗口外的旧记录
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)

-- 2. 统计当前窗口内的请求数
local count = redis.call('ZCARD', key)

if count < limit then
    -- 3. 未超限,记录本次请求
    redis.call('ZADD', key, now, member)
    -- 4. 设置key过期时间,略大于窗口即可
    redis.call('PEXPIRE', key, window)
    return 1
else
    return 0
end

Java侧借助Jedis或RedisTemplate调用这段脚本,示例代码如下:

public boolean tryAcquire(String key, int limit, long windowMillis) {
    String now = String.valueOf(System.currentTimeMillis());
    String member = now + "-" + UUID.randomUUID().toString();
    Object result = redisTemplate.execute(rateLimitScript,
            Collections.singletonList(key),
            String.valueOf(windowMillis),
            String.valueOf(limit),
            now,
            member);
    return "1".equals(String.valueOf(result));
}

调用方拿到tryAcquire的返回值决定放行还是拒绝,被限流的请求可以返回429状态码或者抛出业务异常。注意Lua脚本中的时间戳建议由客户端传入而不是在脚本里调用TIME命令,因为带随机性质的命令在某些主从场景下会导致脚本无法正确复制,虽然新版本Redis已经用效果复制机制解决了这个问题,但显式传参依然是最稳妥的写法。

和其他限流算法的对比与选型建议

漏桶算法以恒定速率放行请求,超出桶容量的直接丢弃,特点是整流效果好,能保证下游以平稳速率被访问,但无法应对正常的短时突发流量。令牌桶则相反,令牌以固定速率生成放入桶中,请求拿到令牌才放行,桶里攒下的令牌允许一定程度的突发,Guava的RateLimiter就是经典实现。滑动窗口介于两者之间,它限制的是任意时间段内的总量,逻辑清晰,语义和“每秒最多N次”这类需求直接对应。

选型上,如果需求是保护脆弱的下游服务,希望流量尽可能平滑,漏桶更合适;如果允许一定突发又要控制平均速率,令牌桶更好;如果只是简单地控制接口调用频次,比如防刷、API配额控制,滑动窗口是最直观的方案。另外要提醒一点,窗口大小的设置要结合业务,比如验证码接口通常按1分钟限制5次,订单查询可能按1秒限制50次,窗口粒度和阈值需要压测验证后再定。

分布式部署时还有几个细节值得注意:所有服务节点必须连接同一个Redis实例或集群,时钟要尽量同步,因为窗口判断依赖时间戳;Redis挂掉时的降级策略要提前想好,可以选择本地限流兜底或者直接放行并记录日志告警;对于超高QPS的接口,ZSET的写入压力和网络开销不可忽视,此时可以考虑换成格子计数法或者直接在网关层做限流。理解了这些原理和取舍,你就能根据自己的业务场景搭建出一套可靠的分布式限流体系。

Redis限流滑动窗口Redis Lua脚本修改时间:2026-09-08 14:41:11

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260908/52821.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。