如何用Redis实现基于IP的滑动窗口限流策略?

摘要:在开发高并发系统时,限流是一个绕不开的话题。无论是为了保护后端服务不被突发流量打垮,还是为了防爬虫、防恶意攻击,限流都是最常用的手段之一。常见的限流算法有计数器(固定窗口)、滑动窗口、漏桶、令牌桶等。今天我们就来聊一聊如何用 Redis 的
引言 在开发高并发系统时,限流是一个绕不开的话题。无论是为了保护后端服务不被突发流量打垮,还是为了防爬虫、防恶意攻击,限流都是最常用的手段之一。常见的限流算法有计数器(固定窗口)、滑动窗口、漏桶、令牌桶等。今天我们就来聊一聊如何用 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 就是请求发生的时间戳(毫秒或秒级,根据精度要求) 每次请求到来时,我们执行以下逻辑: 删除窗口外的数据:ZREMRANGEBYSCORE key 0 (now - 60),移除 60 秒之前的记录。 统计当前窗口内的请求数:ZCARD key。 如果数量 ≥ 100,则拒绝请求。 否则,记录本次请求:ZADD key now requestId。 设置 key 的过期时间(比如 120 秒),避免长期占用内存。 原子性保证 上述步骤需要保证原子性,否则在高并发下可能出现竞争条件:比如两个请求同时删除过期数据,然后都发现当前计数 < 100,都执行了 ZADD,导致限流失效。因此,我们必须把整个逻辑封装在一个 Lua 脚本里,让 Redis 原子执行。
阅读全文