一、问题背景

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


二、限流类型总览

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

三、固定窗口限流

3.1 什么是固定窗口限流

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

|---5min---|---5min---|---5min---|---5min---|
20:00      20:05      20:10      20:15

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

3.2 Redis 实现
-- 固定窗口限流实现
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 示例
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)
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 编写,需要单独安装:

# 下载并编译
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 命令详解
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  # 下次请求的间隔时间
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

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐