大家好,这里是程序员阿亮,今天我来给大家讲解一下常见的限流算法。
前言
限流(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. 工作流程
- 请求到达 → 尝试放入桶中。
- 若桶未满 → 入队(等待处理)。
- 若桶已满 → 拒绝请求。
- 系统以固定速率从桶中取出请求进行处理(如每秒 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.350到10: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 实现,既能保证分布式一致性,又具备较高精度。
- 虽然内存开销略高于固定窗口,但在大多数业务场景中完全可接受。
总结
实际上限流是为了减轻服务器压力,应对高并发场景,比较常用的算法是令牌与滑动窗口算法。