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

在分布式系统和高并发场景中,限流(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 接口限流、网关层防护。
相关推荐
小小测试开发38 分钟前
安装 Python 3.10+
开发语言·人工智能·python
AAA大运重卡何师傅(专跑国道)2 小时前
【无标题】
开发语言·c#
XBodhi.3 小时前
Visual Studio C++ 语法错误: 缺少“;”(在“return”的前面)
开发语言·c++·visual studio
LSssT.3 小时前
【01】Python 机器学习
开发语言·python
心之伊始3 小时前
Java 后端接入大模型:从 Token、并发到推理成本的完整估算方法
java·spring boot·性能优化·大模型·llm
l1t4 小时前
DeepSeek总结的使用实体-组件-系统和基于存在性处理进行Python编程39-40
开发语言·python
BlackTurn4 小时前
技术经理投标
java
曾阿伦4 小时前
Python 搭建简易HTTP服务
开发语言·python·http
YG亲测源码屋4 小时前
java配置环境变量、jdk环境变量配置、java环境变量设置方法
java·开发语言
MIUMIUKK4 小时前
从语法层面,看懂 Python 的特殊处
java·开发语言·python