Redis 实现限流功能的几种方法

一、问题背景

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


二、限流类型总览

限流类型 核心思想 优点 缺点
固定窗口限流 时间窗口固定,到期自动清零 实现简单 存在窗口边界突击流量问题
滑动窗口限流 窗口随时间滑动,统计窗口内请求数 精确解决边界问题 实现稍复杂
漏斗限流 容量固定,速率固定 精确控制容量和速率 需要 Redis 模块支持
令牌桶限流 令牌以固定速率放入桶中 支持突发流量 实现复杂

三、固定窗口限流

3.1 什么是固定窗口限流

将时间划分为固定的窗口,例如每 5 分钟为一个窗口:

复制代码
|---5min---|---5min---|---5min---|---5min---|
20:00      20:05      20:10      20:15

在每个窗口内独立计数,窗口到期后计数清零。

3.2 Redis 实现
lua 复制代码
-- 固定窗口限流实现
local key = "***" .. user_id .. ":" .. action
local 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 复制代码
# Python 示例
pipe = redis.pipeline()
pipe.set(key, 0, ex=period, nx=True)
pipe.incr(key)
res = pipe.execute()
return res[1] <= limit
3.3 固定窗口的局限性

假设 5 分钟内限定 10 次请求:

复制代码
20:04-20:05 发生 9 次请求
20:05-20:06 发生 9 次请求

在 20:04-20:06 这 2 分钟内,实际发生了 18 次请求,远超每 5 分钟 10 次的限制。

问题根源:固定窗口的边界不连续,在边界处可能发生突发流量。


四、滑动窗口限流

4.1 核心思想

滑动窗口的核心是窗口随时间连续滑动,而非固定边界:

复制代码
传统固定窗口:      |-----5min-----|-----5min-----|
                   20:00         20:05         20:10

滑动窗口:
                   现在时刻的窗口持续向前滑动
                   |----5min-----|----5min-----|
                   20:01         20:06
4.2 Redis 实现(ZSET)
lua 复制代码
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_count
end

流程图:

复制代码
时间轴:[--窗口period--|---未来---]
                       ↑now
                       
ZSET 存储:score=时间戳, member=时间戳
ZREMRANGEBYSCORE:删除 score < now-period 的旧记录
ZCARD:统计剩余元素数量,即窗口内请求数
4.3 为什么用 ZSET 而非 LIST
数据结构 适用场景
ZSET 支持按时间范围删除,适合滑动窗口
LIST 只能按索引删除,无法按时间范围清理

五、漏斗限流(Redis-Cell)

5.1 什么是漏斗限流

漏斗限流的核心是容量固定 + 速率固定,能精确控制元素的容量和速率:

复制代码
漏斗模型:
       [入口] -> (容量固定) -> [出口]
         ↓
      速率恒定
  • 漏斗容量:最多能容纳多少请求
  • 漏斗速率:单位时间内能处理多少请求
5.2 Redis-Cell 模块安装

Redis-Cell 是 Redis 的第三方模块,采用 Rust 编写,需要单独安装:

bash 复制代码
# 下载并编译
git clone https://github.com/brandur/redis-cell
cd redis-cell
cargo build --release
cp target/release/libredis_cell.so /path/to/modules/

# 启动 Redis 加载模块
redis-server --loadmodule /path/to/modules/libredis_cell.so
5.3 CL.THROTTLE 命令详解
bash 复制代码
CL.THROTTLE key capacity operations seconds [quota]

参数说明:

参数 含义 示例
key 漏斗容器名称 user:123:login
capacity 漏斗容量(最大容纳请求数) 10
operations 单位时间内的操作次数 5
seconds 单位时间(秒) 60
quota 单次行为消耗的令牌数(可选,默认1) 1

示例:每 60 秒最多 5 次请求,漏斗容量 10

bash 复制代码
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  # 下次请求的间隔时间
5.4 流速计算
复制代码
流速 = operations / seconds = 5 / 60 ≈ 0.083 请求/秒

这意味着每秒只能处理约 0.083 个请求,即约 12 秒处理 1 个请求。


六、令牌桶限流

6.1 核心思想

令牌桶的核心是令牌以固定速率放入桶中

复制代码
令牌桶:
         -> [桶容量] -> 请求消耗令牌 -> 通过
         ↑
      固定速率放入令牌
  • 桶容量:最大令牌数
  • 令牌添加速率:每秒添加多少令牌
  • 请求消耗:每个请求消耗 1 个令牌
6.2 特点
特点 说明
支持突发流量 桶满时可一次性处理多个请求
令牌非即时补充 需要等待令牌生成
6.3 与漏斗限流的区别
对比维度 漏斗限流 令牌桶限流
速率 匀速 匀速(令牌补充)
突发能力 不支持 支持(桶满时)
实现难度 较简单 较复杂

七、四种限流方案对比

维度 固定窗口 滑动窗口 漏斗限流 令牌桶
实现复杂度
边界突击
突发流量支持 不支持 不支持 不支持 支持
精度控制
额外依赖 Redis-Cell

八、面试追问 FAQ

问题 回答要点
Q: 为什么固定窗口需要 SET + INCR 组合? 单独 INCR + EXPIRE 在进程崩溃时可能丢失数据,SET+EXPIRE 原子操作保证一致性
Q: 滑动窗口为什么要设置过期时间为 period+1? 避免窗口边界附近过期导致数据丢失,确保跨窗口的请求仍被统计
Q: 漏斗限流和令牌桶限流各适用于什么场景? 漏斗:需要精确控制速率的 API 限流;令牌桶:允许突发流量的场景(如秒杀)
Q: Redis-Cell 是原子操作吗? 是,CL.THROTTLE 整个命令是原子的,无需担心并发问题
Q: 滑动窗口的 ZSET 会不会无限增长? 不会,每次请求都会清理窗口外的旧数据,且有 expire 保证清理

九、相关题目

题目 考察点
Redis 固定窗口限流如何保证原子性? SET + INCR + Pipeline
滑动窗口限流为什么用 ZSET 而不是 LIST? 按时间范围删除的能力
漏斗限流如何计算流速? operations / seconds
令牌桶和漏斗限流的本质区别? 突发流量支持

十、总结

限流方案 实现难度 精度 突发流量 推荐场景
固定窗口 不支持 简单场景
滑动窗口 不支持 需要精确控制
漏斗限流 不支持 API 限流
令牌桶 支持 秒杀/抢购

核心结论: 根据业务场景选择合适的限流方案,简单场景用固定窗口,精确控制用滑动窗口或漏斗限流,需要突发能力用令牌桶。


根据零声教育教学写作https://github.com/0voice

相关推荐
weixin_453639596 小时前
Docker Redis 本地能 Ping 通但 6379 端口连不上?排查记录与解决
linux·redis
l1t6 小时前
DeepSeek总结的postgresql 数据分析师 vs width_bucket()
数据库·postgresql
米高梅狮子6 小时前
Redis
数据库·redis·mysql·缓存·docker·容器·github
dinl_vin6 小时前
FastAPI 系列 ·(四):数据库集成——SQLAlchemy 2.0 异步 ORM 与 Alembic 迁移
java·数据库·fastapi
坚定信念,勇往无前7 小时前
electron-vite 安装better-sqlite3
javascript·数据库·electron
大明者省7 小时前
Ubuntu22.04 宝塔面板与 XFCE 远程桌面端口兼容性分析
运维·服务器·数据库·笔记
189228048618 小时前
NY379固态MT29F32T08GSLBHL8-36QA:B
大数据·服务器·人工智能·科技·缓存
liudanzhengxi8 小时前
巧用ULN2003A轻松扩展单片机IO口
数据库·mongodb