常见限流算法--【令牌桶】【漏桶】【固定窗口】【滑动窗口】

大家好,这里是程序员阿亮,今天我来给大家讲解一下常见的限流算法。


前言

限流(Rate Limiting)是系统保护的重要手段,用于控制单位时间内请求的处理数量,防止系统因突发流量而崩溃。基本上常见的限流算法有令牌限流、桶限流、固定窗口限流、滑动窗口限流等

一、令牌桶限流(Token Bucket)

令牌限流算法实际上主要就是以下核心算法:

核心思想

关键参数

行为特点

  • 系统以固定的速率向"桶"中添加令牌(token),桶的容量为 capacity
  • 每个请求需要从桶中获取一个令牌才能被处理。
  • 如果桶中有足够的令牌,则请求被允许;否则,请求被拒绝或等待。
  • 若桶已满,新生成的令牌会被丢弃。
  • rate:令牌生成速率(如每秒 10 个令牌)。
  • capacity:桶的最大容量(如最多 20 个令牌)。
  • burst :突发流量能力 = capacity,即一次最多可处理 capacity 个请求。
  • 平滑限流 :长期来看,请求速率不会超过 rate
  • 支持突发 :短时间内可处理最多 capacity 个请求(只要桶中有令牌)。
  • 灵活性高:相比漏桶,更适合真实业务场景(如用户偶尔快速点击

特点:允许一定程度的突发流量(只要桶中有令牌),更灵活,常用于 API 网关(如 Guava RateLimiter)。

实现源码

基于Lua实现,由于Lua保证了一组指令只能被Redis进行原子执行(Redis单线程),所以不会有并发问题

Lua 复制代码
-- KEYS[1] = 限流 key(如 "rate_limit:user:123")
-- ARGV[1] = 当前时间戳(毫秒)
-- ARGV[2] = 令牌生成速率(每秒多少个,如 10)
-- ARGV[3] = 桶容量(如 20)
-- ARGV[4] = 需要消耗的令牌数(通常为 1)

local key = KEYS[1]
local now = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])      -- tokens per second
local capacity = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])

-- 获取当前桶状态
local last_time = redis.call('HGET', key, 'last_time')
local tokens = redis.call('HGET', key, 'tokens')

-- 初始化
if not last_time then
    last_time = now
    tokens = capacity
else
    last_time = tonumber(last_time)
    tokens = tonumber(tokens)
end

-- 计算自上次以来新增的令牌数
local delta = now - last_time
local new_tokens = tokens + delta * rate / 1000.0

-- 不能超过桶容量
if new_tokens > capacity then
    new_tokens = capacity
end

-- 判断是否足够
if new_tokens >= requested then
    -- 消耗令牌
    new_tokens = new_tokens - requested
    -- 更新状态
    redis.call('HSET', key, 'last_time', now, 'tokens', new_tokens)
    -- 设置过期时间(可选,避免冷 key 长期存在)
    redis.call('EXPIRE', key, math.ceil(capacity / rate) + 2)
    return 1  -- 允许
else
    -- 不更新状态(或可选择更新时间但不减令牌,视需求而定)
    return 0  -- 拒绝
end

二、漏桶算法(Leaky Bucket)

1. 核心思想

  • 想象一个固定容量的水桶 ,底部有一个小孔,水以恒定速率从孔中漏出。
  • 请求相当于"水",被倒入桶中。
  • 如果桶未满,请求可以进入(排队等待处理);
  • 如果桶已满,新请求会被丢弃(拒绝)
  • 桶中的请求以固定速率被处理(即"漏水"速率)。

关键点 :漏桶控制的是输出速率,而不是输入速率。

2. 工作流程

  1. 请求到达 → 尝试放入桶中。
  2. 若桶未满 → 入队(等待处理)。
  3. 若桶已满 → 拒绝请求。
  4. 系统以固定速率从桶中取出请求进行处理(如每秒 10 个)。

与令牌桶的区别

漏桶是请求进入桶中排队,系统以固定速率从桶底"漏水"(处理请求)

令牌桶是以固定速率往桶里放"令牌",请求必须拿到令牌才能通过

  • 漏桶 抑制突发(强制平滑)
  • 令牌桶 允许突发(只要桶里有令牌)

Redis实现

cpp 复制代码
# 请求入桶(LPUSH)
LPUSH leaky_bucket:api1 request_id

# 检查桶大小(避免溢出)
LTRIM leaky_bucket:api1 0 99  # 限制最多100个

# 后台定时任务(如每100ms):
BRPOP leaky_bucket:api1 0   # 阻塞弹出,模拟"漏水"

三、固定窗口算法(Fixed Window Rate Limiting)

概念&思想

固定窗口算法将时间划分为固定长度的时间窗口(如 1 秒、1 分钟),在每个窗口内统计请求次数。如果请求数超过预设阈值,则拒绝后续请求,直到进入下一个窗口。

临界突刺问题--Boundary Burst

  • 限流规则:1 秒最多 2 次
  • 用户在 00:00:00.900 发送 2 次请求 → 被第 0 秒窗口接受
  • 紧接着在 00:00:01.001 又发送 2 次请求 → 被第 1 秒窗口接受
  • 结果 :在 [00:00:00.900, 00:00:01.001]0.101 秒内处理了 4 次请求,远超 2 次/秒的限制!

这实际上就在一个临界时间段接受了双倍请求

代码实现

Lua 复制代码
-- KEYS[1] = 限流 key(如 "api:user123")
-- ARGV[1] = 窗口大小(秒)
-- ARGV[2] = 限流阈值

local current = redis.call("INCR", KEYS[1])
if current == 1 then
    -- 第一次访问,设置过期时间为窗口长度
    redis.call("EXPIRE", KEYS[1], tonumber(ARGV[1]))
end
if current <= tonumber(ARGV[2]) then
    return 1  -- 允许
else
    return 0  -- 拒绝
end

四、滑动窗口限流(Sliding Window

概念&思想

滑动窗口限流(Sliding Window Rate Limiting)是一种高精度、低突刺风险 的限流算法,旨在解决固定窗口算法在时间边界处的流量突刺问题,同时兼顾实现效率与控制粒度。

以当前请求时间为基准,动态查看过去 T 秒内的所有请求次数

  • 不再依赖"对齐的时间格子",而是滑动地维护一个长度为 T 的时间窗口
  • 每次请求到来时,只统计 [now - T, now] 区间内的请求数。
  • 若 ≤ 阈值,则放行;否则拒绝。

示例:

  • 规则:最近 1 秒最多 5 次
  • 当前时间:10:00:01.350
  • 系统检查 10:00:00.35010:00:01.350 之间的请求总数。
  • 即使跨越了"整秒"边界,也能精确控制。

实现方式

1. 连续滑动窗口(精确版)

  • 保存每个请求的精确时间戳
  • 每次请求时,遍历/清理过期时间戳,统计有效请求数。
数据结构:
  • 使用双端队列(Deque)有序列表 存储时间戳。
java 复制代码
Queue<Long> window = new LinkedList<>();
int limit = 5;
long windowMs = 1000; // 1秒

boolean tryAcquire() {
    long now = System.currentTimeMillis();
    
    // 移除窗口外的旧记录
    while (!window.isEmpty() && now - window.peek() > windowMs) {
        window.poll();
    }
    
    if (window.size() < limit) {
        window.offer(now);
        return true;  // 允许
    } else {
        return false; // 拒绝
    }
}

2. 分段滑动窗口(近似版 / Rolling Window)

  • 将时间窗口划分为多个小格子(buckets),如 1 秒窗口分成 10 个 100ms 的格子。
  • 每个格子记录该时间段内的请求次数。
  • 请求到来时,只累加未过期格子的计数。
示例(1 秒窗口,10 个格子):
  • 当前时间:10:00:01.350
  • 所属格子索引:350ms / 100ms = 3(第 3 个格子)
  • 需要累加的格子:从 (now - 1000ms)now 对应的所有格子(可能跨多个周期)
Redis 实现:

使用 Sorted Set(ZSET) 存储时间戳,或使用 Hash + 格子编号

ZSET 存时间戳(接近连续滑动窗口)
Lua 复制代码
-- KEYS[1] = 限流 key
-- ARGV[1] = 当前时间戳(毫秒)
-- ARGV[2] = 窗口大小(毫秒)
-- ARGV[3] = 限流阈值

local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])

-- 删除过期请求(时间戳 < now - window)
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)

-- 获取当前窗口内请求数
local count = redis.call('ZCARD', key)

if count < limit then
    redis.call('ZADD', key, now, now)  -- 添加当前请求时间戳
    redis.call('EXPIRE', key, math.ceil(window / 1000) + 1)
    return 1
else
    return 0
end
  • 它通过动态维护最近 T 秒的请求记录,彻底解决了固定窗口的边界突刺问题。
  • 安全敏感、防刷、高频 API 限流等场景中,是比固定窗口更可靠的选择。
  • 基于 Redis 的 ZSET + Lua 实现,既能保证分布式一致性,又具备较高精度。
  • 虽然内存开销略高于固定窗口,但在大多数业务场景中完全可接受。

总结

实际上限流是为了减轻服务器压力,应对高并发场景,比较常用的算法是令牌与滑动窗口算法。

相关推荐
马尔代夫哈哈哈2 小时前
Spring Mvc(二)
java·spring boot·spring·servlet·java-ee
橙露2 小时前
C语言执行四大流程详解:从源文件到可执行程序的完整生命周期
java·c语言·开发语言
啊阿狸不会拉杆2 小时前
《计算机操作系统》第六章-输入输出系统
java·开发语言·c++·人工智能·嵌入式硬件·os·计算机操作系统
独自破碎E2 小时前
动态规划-打家劫舍I-II-III
算法·动态规划
云草桑2 小时前
C#.net 分布式ID之雪花ID,时钟回拨是什么?怎么解决?
分布式·算法·c#·.net·雪花id
风筝在晴天搁浅2 小时前
hot100 104.二叉树的最大深度
java·算法
潇冉沐晴2 小时前
div3 970个人笔记
c++·笔记·算法
晔子yy2 小时前
说一下Java的垃圾回收机制
java·开发语言
历程里程碑2 小时前
双指针1:移动零
大数据·数据结构·算法·leetcode·elasticsearch·搜索引擎·散列表