在实际业务场景中,限流是保护系统的重要手段:在一段时间(period)内,限定某个行为(action)的最大次数(max_count)。本文介绍如何基于 Redis 实现多种限流方案。

| 限流类型 | 核心思想 | 优点 | 缺点 |
|---|---|---|---|
| 固定窗口限流 | 时间窗口固定,到期自动清零 | 实现简单 | 存在窗口边界突击流量问题 |
| 滑动窗口限流 | 窗口随时间滑动,统计窗口内请求数 | 精确解决边界问题 | 实现稍复杂 |
| 漏斗限流 | 容量固定,速率固定 | 精确控制容量和速率 | 需要 Redis 模块支持 |
| 令牌桶限流 | 令牌以固定速率放入桶中 | 支持突发流量 | 实现复杂 |
将时间划分为固定的窗口,例如每 5 分钟为一个窗口:
|---5min---|---5min---|---5min---|---5min---|20:00 20:05 20:10 20:15
在每个窗口内独立计数,窗口到期后计数清零。
-- 固定窗口限流实现local key = "***" .. user_id .. ":" .. actionlocal limit = 10 -- 最大次数local period = 10 -- 时间窗口(秒)-- 方式1:INCR + EXPIRE(存在问题)redis.call('INCR', key)redis.call('EXPIRE', key, period)-- 方式2:SET + INCR(正确实现,解决竞态条件)-- 使用 SET + EXPIRE 原子操作,避免窗口切换时丢失数据redis.call('SET', key, 0, 'EX', period, 'NX')local count = redis.call('INCR', key)return count <= limit关键点:使用 SET + EXPIRE 代替单独 EXPIRE,避免 INCR 和 EXPIRE 之间进程崩溃导致数据丢失。
可通过 Pipeline 保证两个命令同时发送:
# Python 示例pipe = redis.pipeline()pipe.set(key, 0, ex=period, nx=True)pipe.incr(key)res = pipe.execute()return res[1] <= limit
假设 5 分钟内限定 10 次请求:
20:04-20:05 发生 9 次请求20:05-20:06 发生 9 次请求
在 20:04-20:06 这 2 分钟内,实际发生了 18 次请求,远超每 5 分钟 10 次的限制。
问题根源:固定窗口的边界不连续,在边界处可能发生突发流量。
滑动窗口的核心是窗口随时间连续滑动,而非固定边界:
传统固定窗口: |-----5min-----|-----5min-----| 20:00 20:05 20:10滑动窗口: 现在时刻的窗口持续向前滑动 |----5min-----|----5min-----| 20:01 20:06
local function is_action_allowed(red, user_id, action, period, max_count) local key = "***" .. user_id .. ":" .. action local now = redis.call('TIME') -- 获取当前时间戳(毫秒) -- 1. 记录当前行为(score 和 member 都用时间戳) red:zadd(key, now, now) -- 2. 移除窗口之前的行为记录 red:zremrangebyscore(key, 0, now - period * 1000) -- 3. 获取窗口内的行为数量 local count = red:zcard(key) -- 4. 设置过期时间,避免冷用户持续占用内存 red:expire(key, period + 1) return count <= max_countend流程图:
时间轴:[--窗口period--|---未来---] ↑now ZSET 存储:score=时间戳, member=时间戳ZREMRANGEBYSCORE:删除 score < now-period 的旧记录ZCARD:统计剩余元素数量,即窗口内请求数
| 数据结构 | 适用场景 |
|---|---|
| ZSET | 支持按时间范围删除,适合滑动窗口 |
| LIST | 只能按索引删除,无法按时间范围清理 |
漏斗限流的核心是容量固定 + 速率固定,能精确控制元素的容量和速率:
漏斗模型: [入口] -> (容量固定) -> [出口] ↓ 速率恒定
Redis-Cell 是 Redis 的第三方模块,采用 Rust 编写,需要单独安装:
# 下载并编译git clone https://github.com/brandur/redis-cellcd redis-cellcargo build --releasecp target/release/libredis_cell.so /path/to/modules/# 启动 Redis 加载模块redis-server --loadmodule /path/to/modules/libredis_cell.so
CL.THROTTLE key capacity operations seconds [quota]
参数说明:
| 参数 | 含义 | 示例 |
|---|---|---|
| key | 漏斗容器名称 | user:123:login |
| capacity | 漏斗容量(最大容纳请求数) | 10 |
| operations | 单位时间内的操作次数 | 5 |
| seconds | 单位时间(秒) | 60 |
| quota | 单次行为消耗的令牌数(可选,默认1) | 1 |
示例:每 60 秒最多 5 次请求,漏斗容量 10
CL.THROTTLE user:123:login 10 5 60
返回结果:
1) (integer) 0 # 是否被限流(0=允许,1=拒绝)2) (integer) 7 # 漏斗剩余容量3) (integer) 7 # 如果被拒绝,还需要等多久(秒)4) (integer) -1 # 预留字段5) (integer) 60 # 下次请求的间隔时间
流速 = operations / seconds = 5 / 60 ≈ 0.083 请求/秒
这意味着每秒只能处理约 0.083 个请求,即约 12 秒处理 1 个请求。
令牌桶的核心是令牌以固定速率放入桶中:
令牌桶: -> [桶容量] -> 请求消耗令牌 -> 通过 ↑ 固定速率放入令牌
| 特点 | 说明 |
|---|---|
| 支持突发流量 | 桶满时可一次性处理多个请求 |
| 令牌非即时补充 | 需要等待令牌生成 |
| 对比维度 | 漏斗限流 | 令牌桶限流 |
|---|---|---|
| 速率 | 匀速 | 匀速(令牌补充) |
| 突发能力 | 不支持 | 支持(桶满时) |
| 实现难度 | 较简单 | 较复杂 |
| 维度 | 固定窗口 | 滑动窗口 | 漏斗限流 | 令牌桶 |
|---|---|---|---|---|
| 实现复杂度 | 低 | 中 | 低 | 高 |
| 边界突击 | 有 | 无 | 无 | 无 |
| 突发流量支持 | 不支持 | 不支持 | 不支持 | 支持 |
| 精度控制 | 低 | 中 | 高 | 高 |
| 额外依赖 | 无 | 无 | Redis-Cell | 无 |
| 问题 | 回答要点 |
|---|---|
| Q: 为什么固定窗口需要 SET + INCR 组合? | 单独 INCR + EXPIRE 在进程崩溃时可能丢失数据,SET+EXPIRE 原子操作保证一致性 |
| Q: 滑动窗口为什么要设置过期时间为 period+1? | 避免窗口边界附近过期导致数据丢失,确保跨窗口的请求仍被统计 |
| Q: 漏斗限流和令牌桶限流各适用于什么场景? | 漏斗:需要精确控制速率的 API 限流;令牌桶:允许突发流量的场景(如秒杀) |
| Q: Redis-Cell 是原子操作吗? | 是,CL.THROTTLE 整个命令是原子的,无需担心并发问题 |
| Q: 滑动窗口的 ZSET 会不会无限增长? | 不会,每次请求都会清理窗口外的旧数据,且有 expire 保证清理 |
| 限流方案 | 实现难度 | 精度 | 突发流量 | 推荐场景 |
|---|---|---|---|---|
| 固定窗口 | 低 | 低 | 不支持 | 简单场景 |
| 滑动窗口 | 中 | 中 | 不支持 | 需要精确控制 |
| 漏斗限流 | 低 | 高 | 不支持 | API 限流 |
| 令牌桶 | 高 | 高 | 支持 | 秒杀/抢购 |
核心结论: 根据业务场景选择合适的限流方案,简单场景用固定窗口,精确控制用滑动窗口或漏斗限流,需要突发能力用令牌桶。
以上就是Redis实现限流功能的几种方法总结的详细内容,更多关于Redis实现限流功能的资料请关注本站其它相关文章!
您可能感兴趣的文章: