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

在分布式系统和高并发场景中,限流(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,其中 score 和 member 都存时间戳。

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 接口限流、网关层防护。
相关推荐
2501_944521593 小时前
Ramda 跨端三方库仓颉适配全流程
开发语言·javascript·flutter·php
行思理3 小时前
go语言学习难度及适合学习的人群
开发语言·学习·golang
wangchunyu1144 小时前
JAVA CompletableFuture异步编程实战
java·开发语言·python
paopaokaka_luck5 小时前
流浪动物救助与收养系统(DeepSeek AI对话、 ECharts多维分析、救助流程衔接、领养与回访衔接、 捐赠确认与公示、志愿者协作)
java·前端·javascript·人工智能·mysql·echarts·推荐算法
Thomas.Sir5 小时前
第9课:SpringBoot4.x Web开发基础:WebMvc自动配置、静态资源、请求参数接收
java·spring boot
程序 代码狂人5 小时前
切片表与分区表的区别(数据开发面试版)
java·大数据·开发语言·数据仓库·算法
2501_944521596 小时前
Deepmerge 跨端三方库仓颉适配全流程
java·python·eclipse·github
meiying88387 小时前
快递单号批量查询Python脚本:从手动到自动化的完整实现
开发语言·python·自动化
@大迁世界7 小时前
告别 Go,拥抱 Rust
开发语言·golang·rust
SEO_juper7 小时前
Node.js 重定向链检测工具:301 跳了几跳、有没有成环,一次看清
xml·开发语言·python·node.js·json·bash