当前位置: 首页 > news >正文

(面试题)Redis实现 IP 维度滑动窗口限流实践

引言

在开发高并发系统时,限流是一个绕不开的话题。无论是为了保护后端服务不被突发流量打垮,还是为了防爬虫、防恶意攻击,限流都是最常用的手段之一。常见的限流算法有计数器(固定窗口)、滑动窗口、漏桶、令牌桶等。今天我们就来聊一聊如何用 Redis 的有序集合(ZSET)实现一个滑动窗口限流,并以 IP 维度限制 60 秒内最多 100 次请求为例,给出完整的设计思路和代码。

需求描述

假设我们有一个公开的 API,需要根据调用方的 IP 地址进行限流:

  • 任意时刻向前推 60 秒(滑动窗口)
  • 同一个 IP 最多允许 100 次请求

这里强调“任意时刻”,意味着我们不能用固定时间窗口(比如每分钟重置一次),因为固定窗口在边界处可能允许瞬间两倍的流量。比如:

  • 12:30:59 请求了 100 次
  • 12:31:00 又请求了 100 次

那么在 12:30:30 ~ 12:31:30 这 60 秒内,实际发生了 200 次请求,显然违背了我们的限制。所以必须用滑动窗口来精确控制。

为什么不用 INCR 做固定窗口?

很多初学者会想到用 Redis 的 INCR 配合过期时间来实现限流:

INCR limit:ip:192.168.1.1:202503051230   # 按分钟分片
EXPIRE limit:ip:192.168.1.1:202503051230 60

这种做法本质是固定窗口:每分钟一个计数器,窗口切换时计数器重置。问题就在于窗口切换的那一瞬间,前后两个窗口的请求可能叠加,导致实际 QPS 翻倍。虽然你可以把窗口粒度调小(比如按秒分片),但依然存在边界突刺,而且 key 的数量会爆炸。所以,要实现严格的滑动窗口,必须记录每一次请求的时间戳

滑动窗口设计:基于 Redis ZSET

核心思路

利用有序集合(ZSET)的特性:

  • 每个 IP 对应一个 ZSET,key 设计为 limit:ip:{ip}
  • ZSET 的 member 可以用唯一请求 ID(比如 UUID 或 请求时间戳+随机数),但为了简单,通常直接用 当前时间戳 作为 member 也可以(如果同一毫秒内有多个请求,member 会重复,但概率极低,也可以用 时间戳_递增计数 保证唯一)
  • ZSET 的 score 就是请求发生的时间戳(毫秒或秒级,根据精度要求)

每次请求到来时,我们执行以下逻辑:

  1. 删除窗口外的数据:ZREMRANGEBYSCORE key 0 (now - 60),移除 60 秒之前的记录。
  2. 统计当前窗口内的请求数:ZCARD key。
  3. 如果数量 ≥ 100,则拒绝请求。
  4. 否则,记录本次请求:ZADD key now requestId。
  5. 设置 key 的过期时间(比如 120 秒),避免长期占用内存。

原子性保证

上述步骤需要保证原子性,否则在高并发下可能出现竞争条件:比如两个请求同时删除过期数据,然后都发现当前计数 < 100,都执行了 ZADD,导致限流失效。因此,我们必须把整个逻辑封装在一个 Lua 脚本里,让 Redis 原子执行。

下面是一个简单的 Lua 脚本示例(使用秒级时间戳):

-- KEYS[1] = key
-- ARGV[1] = 当前时间戳(秒)
-- ARGV[2] = 窗口大小(秒)
-- ARGV[3] = 最大请求数
-- ARGV[4] = requestId(可选,也可以用时间戳+随机数生成)local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local requestId = ARGV[4] or tostring(now) .. '_' .. tostring(math.random(1000000))-- 移除窗口外的数据
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)-- 统计当前窗口请求数
local current = redis.call('ZCARD', key)if current >= limit thenreturn 0  -- 拒绝
else-- 记录本次请求redis.call('ZADD', key, now, requestId)-- 设置过期时间(窗口大小*2,避免key长期存在)redis.call('EXPIRE', key, window * 2)return 1  -- 允许
end

调用时,用 EVALSHAEVAL 传入参数即可。这个脚本返回 1 表示放行,0 表示限流。

为什么 requestId 不能直接用时间戳?

如果多个请求在同一秒内到达,使用相同的时间戳作为 member 会导致 ZADD 时只保留最后一个,丢失之前的计数。所以必须保证 member 唯一。可以用 时间戳_递增序号 或者 UUID。如果并发量不大,也可以直接用时间戳,但为了严谨,还是加上随机后缀为好。

应用场景扩展

这个基于 ZSET 的滑动窗口限流,除了 IP 维度,还可以用在很多地方:

  1. API 限流:按用户 ID、AppKey 等维度,限制单位时间内调用次数。
  2. 登录防爆破:限制同一 IP 或同一账号的失败尝试次数,比如 5 分钟允许 5 次登录失败。
  3. 短信验证码发送限制:防止短信轰炸,同一手机号 1 分钟内最多发送 1 条。
  4. 爬虫防护:对爬虫 IP 进行动态封禁,如果某 IP 在短时间内请求量过大,则返回验证码或直接拒绝。
  5. 商品秒杀/活动防刷:限制同一用户参加秒杀的频率。
  6. 评论/点赞防刷:限制同一用户对同一内容的操作次数。

它的优点是精确、无边界突刺,缺点是需要存储每一次请求的记录,如果限流阈值很大(比如每秒 10 万次),ZSET 会变得非常庞大,内存占用和性能都会成为问题。因此,它适用于阈值较低的场景(比如几十到几百次/分钟)。对于超高阈值的限流,可以采用令牌桶或漏斗算法,这些算法内存占用更小,但精度稍低。

注意事项

  1. 时间同步:所有请求的时间戳最好由 Redis 服务器生成,或者使用同一个时间源(如 NTP),避免因客户端时间不一致导致窗口错乱。上面的 Lua 脚本中,时间戳由调用方传入,如果调用方时间不准,会影响限流效果。建议在脚本内使用 redis.call('TIME') 获取 Redis 服务器时间。
  2. 内存清理:虽然我们设置了过期时间,但 ZSET 的 member 会一直保留到过期。如果窗口大小是 60 秒,过期时间设为 120 秒,那么每个 key 最多保留 120 秒的数据。即便如此,如果 IP 量很大,Redis 内存消耗依然可观。可以结合业务特点,对非活跃 IP 及时清理。
  3. 性能考虑:ZREMRANGEBYSCORE 和 ZADD 都是 O(log N) 操作,N 是窗口内的请求数。如果阈值是 100,N 就是 100,性能完全没问题。但如果阈值是 10000,ZSET 操作会变慢,此时可以考虑改用哈希结构或别的算法。
  4. 分布式环境:如果应用是多实例部署,所有实例共享同一个 Redis,上面的设计天然支持分布式限流。但要注意 Redis 的单点故障,可以配合 Redis 集群或哨兵提高可用性。
  5. 降级方案:当 Redis 不可用时,限流组件应该能快速失败(比如拒绝请求)或降级为本地限流,避免服务雪崩。

总结

通过 Redis 的 ZSET 实现滑动窗口限流,代码简单,逻辑清晰,能够精确控制任意时间窗口内的请求量,完美规避固定窗口的边界突刺问题。虽然需要存储每一次请求的记录,但在阈值不大的场景下非常实用。结合 Lua 脚本的原子性,我们可以放心地在生产环境中使用。

希望这篇文章能帮你理解滑动窗口限流的原理和实现。如果你有更好的想法或问题,欢迎留言讨论!

http://www.jsqmd.com/news/439076/

相关文章:

  • 2026年3月,为你呈现评价好的钢承口水泥管生产厂家排行,3米水泥管/大型顶管/混凝土管顶管,水泥管直销厂家口碑推荐 - 品牌推荐师
  • 2026年链轮厂家推荐:山东达源机械装备有限公司,提升机链轮/NSE提升机链轮/锯齿链轮/非标链轮/捞渣机链轮全系供应 - 品牌推荐官
  • 健康远程咨询服务,创新智慧就医体验
  • 2026凤凰职教学培课堂怎么样?课程特色与学员反馈解析 - 品牌排行榜
  • 如何选择台式超速离心机?一份涵盖品牌、性能与售后的终极指南 - 品牌推荐大师1
  • 西恩士 检测清洁度仪品牌榜首 高端清洁度检测设备厂家优选 - 技术权威说
  • 企阳餐饮展活动精彩不,2026时间表和展会选购要点分享 - 工业品牌热点
  • 2026年西安楼房销售/西安买房/西安楼盘开发商盘点与购房趋势观察:陕西地建嘉信置业有限公司 - 2026年企业推荐榜
  • 2026内江自贡年度专业管理露营地排名,露营地服务选哪家好? - mypinpai
  • 2026年五年一贯制专转本机构有哪些?选择参考 - 品牌排行榜
  • 洁净度检测设备哪家好 西恩士稳居行业排行榜首 - 仪器权威论
  • 洁净度测仪品牌怎么选 西恩士工业堪称行业标杆 - 技术权威说
  • 西恩士 检测清洁度的仪器头部品牌 高端制造清洁度系统优选推荐 - 技术权威说
  • [2026 LSF/MM/BPF TOPIC] Ways to mitigate limitations of percpu memory allocator 前瞻
  • 织梦DedeCms忘记管理员密码怎么办?重置还是找回,附修改方法和代码
  • 基于STM32的汽车水温及车内温度检查系统设计
  • 聊聊热重分析仪质量好的品牌,汇诚在南京口碑咋样? - 工业推荐榜
  • 2026年天津离婚纠纷律师电话查询推荐:精选推荐与使用指南 - 品牌推荐
  • 2026膜结构雨棚优质厂家推荐指南:膜结构设计安装公司、膜结构遮阳棚厂家、遮阳篷膜结构厂家、遮阳膜结构厂家、骨架膜结构厂家选择指南 - 优质品牌商家
  • 2026年初上海保姆服务市场盘点:为什么选择勤人公社的家政伙伴? - 2026年企业推荐榜
  • Emlog忘记管理员密码怎么办?重置还是找回,附修改方法和代码
  • 真的太省时间 10个降AIGC平台测评:继续教育降AI率必备工具推荐
  • 上海全屋定制攻略:2026原木风定制厂家精选,原木风装饰设计/复古精装房设计/现代简约室内设计,上海全屋定制公司推荐榜 - 品牌推荐师
  • 《B3834 [GESP202303 一级] 长方形面积》
  • int index = vars.get(__jm__Loop Controller__idx) as int
  • 25 年中国卖出 145.4 万台智能眼镜;主动式 AI 耳机光帆科技再融资,总融资额近 3 亿丨日报
  • 2026年徐汇区婚介所排名,婚介所哪家好排名揭晓 - 工业设备
  • 照着用就行:专科生必备的AI论文软件 —— 千笔ai写作
  • 从“看摊儿”到“看盘”:美团核销接口如何点亮儿童乐园的“平淡时光”
  • Copilot 命令行使用(npm)