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

在分布式系统和高并发场景中,限流(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 接口限流、网关层防护。
相关推荐
凭君语未可2 小时前
Java 中的实现类是什么
java·开发语言
He少年2 小时前
【基础知识、Skill、Rules和MCP案例介绍】
java·前端·python
wearegogog1232 小时前
离散系统参数辨识与广义预测控制MATLAB实现
开发语言·matlab
史迪仔01122 小时前
[QML] QML IMage图像处理
开发语言·前端·javascript·c++·qt
克里斯蒂亚诺更新2 小时前
myeclipse的pojie
java·ide·myeclipse
迷藏4942 小时前
**eBPF实战进阶:从零构建网络流量监控与过滤系统**在现代云原生架构中,**网络可观测性**和**安全隔离**已成为
java·网络·python·云原生·架构
迷藏4942 小时前
**发散创新:基于Solid协议的Web3.0去中心化身份认证系统实战解析**在Web3.
java·python·web3·去中心化·区块链
qq_433502182 小时前
Codex cli 飞书文档创建进阶实用命令 + Skill 创建&使用 小白完整教程
java·前端·飞书