常见的限流算法和实现原理

在分布式系统和高并发场景中,限流(Rate Limiting)是保护系统不被击垮的重要手段。常见的限流算法主要分为四种,其设计思想和应用场景各有侧重。


1. 固定窗口算法 (Fixed Window)

这是最简单的限流算法,将时间划分为固定的周期(窗口)。

  • 原理:在每个固定的时间窗口内限制请求数量。例如,限制每分钟只能处理 100 个请求。如果当前窗口的计数器达到 100,则拒绝后续请求;当进入下一分钟时,计数器清零。

  • 优点:实现简单,内存占用极低。

  • 缺点临界突刺问题。如果在第一分钟的最后 1 秒进来了 100 个请求,第二分钟的第 1 秒又进来了 100 个请求,那么在短短 2 秒内系统处理了 200 个请求,超出了设定的阈值。


2. 滑动窗口算法 (Sliding Window)

为了解决固定窗口的临界问题,滑动窗口将时间细分为多个小格。

  • 原理:将大窗口(如 1 分钟)拆分为更小的时间片段(如 6 个 10 秒的小格)。随着时间的推移,窗口向右滑动,计数器仅统计当前滑动窗口内的总请求数。

  • 优点:平滑了限流过程,窗口划分越细,限流越精准,解决了临界突刺问题。

  • 实现 :通常使用 Redis 的 ZSET 实现,通过 score 记录请求时间戳,统计 count 时移除窗口外的数据。


3. 漏桶算法 (Leaky Bucket)

漏桶算法强制执行恒定的处理速率,主要用于流量整形。

  • 原理:请求像水一样进入桶中,桶底有一个固定的小孔匀速漏出(处理请求)。如果水流入的速度超过了漏出的速度,多出的水会留在桶内;如果桶满了,新进入的水(请求)直接溢出(被拒绝)。

  • 优点:输出频率非常稳定,能绝对保证处理速度不会超过系统负载。

  • 缺点 :无法应对突发流量(Burst Traffic)。即便系统资源当前很空闲,请求也必须按死板的速度排队通过。


4. 令牌桶算法 (Token Bucket)

这是目前业界最主流的限流算法,如 Google 的 Guava RateLimiter 就是基于此实现的。

  • 原理

    1. 系统以恒定速率向桶中放入"令牌"。

    2. 桶有最大容量,满后令牌丢弃。

    3. 每个请求进来必须先从桶里抢到一个令牌,抢不到则拒绝或等待。

  • 优点 :既能限制平均处理速率,又允许一定程度的突发流量(只要桶里还有存量令牌,就可以瞬间处理一波请求)。

  • 实现 :不需要真的起一个定时器放令牌,通常通过 计算时间差 * 生成速率 来动态计算当前可用令牌数。


常见实现方案对照

方案 核心技术 适用场景
Guava RateLimiter 令牌桶 单机限流,适用于 Java 业务逻辑保护。
Sentinel / Hystrix 滑动窗口/令牌桶 微服务架构,支持熔断、降级与限流。
Nginx (limit_req) 漏桶算法 入口层限流,防止恶意爬虫或 CC 攻击。
Redis + Lua 滑动窗口/令牌桶 分布式限流,保证多实例下的全局频次限制。

追问1:redis实现一个

要在 Redis 中实现限流,最稳妥且高效的方式是使用 Redis + Lua 脚本 。Lua 脚本能保证"读取-判断-增加"这一系列操作的原子性,避免在分布式高并发下的竞态条件。

以下是两种最常用的实现方案:


1. 滑动窗口限流 (准确度高)

滑动窗口可以避免固定窗口在时间临界点出现的双倍流量问题。我们利用 Redis 的 ZSET(有序集合)来实现。

  • Key : 接口标识(如 limit:user:123:api

  • Value : ZSET,其中 scoremember 都存时间戳。

Lua 脚本逻辑:

Lua

复制代码
-- KEYS[1]: 限流 key
-- ARGV[1]: 当前时间戳 (毫秒)
-- ARGV[2]: 窗口大小 (毫秒)
-- ARGV[3]: 最大限制数量

-- 1. 移除窗口时间之外的旧数据
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1] - ARGV[2])

-- 2. 统计当前窗口内的请求数
local current_count = redis.call('ZCARD', KEYS[1])

-- 3. 判断是否超过限制
if tonumber(current_count) < tonumber(ARGV[3]) then
    -- 未超限,添加当前请求
    redis.call('ZADD', KEYS[1], ARGV[1], ARGV[1])
    -- 设置 key 过期时间(略大于窗口期即可,防止空间浪费)
    redis.call('PEXPIRE', KEYS[1], ARGV[2])
    return 1
else
    -- 已超限
    return 0
end

2. 令牌桶限流 (性能最优,支持突发)

令牌桶不需要记录每个请求的时间戳,内存开销更小。它的核心思想是:不真的去起定时器发令牌,而是在请求进来时动态计算应该有多少令牌。

Lua 脚本逻辑:

Lua

Lua 复制代码
-- KEYS[1]: 限流 key
-- ARGV[1]: 令牌填充速率 (每秒生成的数量)
-- ARGV[2]: 桶的容量
-- ARGV[3]: 当前时间戳 (秒)
-- ARGV[4]: 请求需要的令牌数 (通常为 1)

local bucket = redis.call('HMGET', KEYS[1], 'tokens', 'last_time')
local tokens = tonumber(bucket[1])
local last_time = tonumber(bucket[2])

-- 如果是第一次访问,初始化
if last_time == nil then
    tokens = tonumber(ARGV[2])
    last_time = tonumber(ARGV[3])
else
    -- 计算时间差内生成的令牌
    local delta_time = math.max(0, ARGV[3] - last_time)
    local generated = delta_time * ARGV[1]
    tokens = math.min(tonumber(ARGV[2]), tokens + generated)
    last_time = ARGV[3]
end

local result = 0
if tokens >= tonumber(ARGV[4]) then
    -- 令牌足够,扣减并允许通过
    tokens = tokens - ARGV[4]
    result = 1
end

-- 更新桶状态
redis.call('HMSET', KEYS[1], 'tokens', tokens, 'last_time', last_time)
redis.call('EXPIRE', KEYS[1], 10) -- 自动清理
return result

3. 两种方案对比

方案 优点 缺点 适用场景
滑动窗口 非常精确,能够严格限制任意时间跨度内的流量。 ZSET 随着请求量增大会占用更多内存。 需要强一致性、精确限制的场景,如短信发送频次。
令牌桶 内存占用低(仅存几个数值),支持应对短时间的突发流量。 无法保证"绝对平滑"的流量,允许一定程度的倾斜。 常见的 API 接口限流、网关层防护。
相关推荐
大飞记Python1 天前
【2026更新】Python基础学习指南(AI版)——04数据类型
开发语言·人工智能·python
极创信息1 天前
信创产品认证怎么做?信创产品测试认证的主要流程
java·大数据·数据库·金融·软件工程
SamDeepThinking1 天前
并发量就算只有2,该上锁还得上呀
java·后端·架构
Alice-YUE1 天前
【js高频八股】防抖与节流
开发语言·前端·javascript·笔记·学习·ecmascript
Sam_Deep_Thinking1 天前
如何让订单系统和营销系统解耦
java·架构·系统架构
云泽8081 天前
C++11 核心特性全解:列表初始化、右值引用与移动语义实战
开发语言·c++
froginwe111 天前
DOM 加载函数
开发语言
lzhdim1 天前
SQL 入门 12:SQL 视图:创建、修改与可更新视图
java·大数据·服务器·数据库·sql
Hello eveybody1 天前
介绍一下背包DP(Python)
开发语言·python·动态规划·dp·背包dp
AI进化营-智能译站1 天前
ROS2 C++开发系列12-用多态与虚函数构建可扩展的ROS2机器人行为模块
开发语言·c++·ai·机器人