常用限流算法详解

常用限流算法详解(含落地方案与代码)

适用场景:API 网关 / Spring Boot 服务 / MQ 消费者 / 定时任务触发 / 防刷防爆破

目标:在不拖垮系统的前提下,给用户稳定、可预期的服务质量(SLA)。


0. 先讲清楚:限流到底在限制什么?

你要先选"限流维度",否则算法再好也会变成玄学。

常见维度:

  • 按接口/pay/create 每秒最多 200 次
  • 按用户/账号:同一 userId 每分钟最多 60 次
  • 按 IP:同一 IP 每秒最多 20 次(防刷常用)
  • 按商户/租户:同一 mchNo 每秒最多 50 次(多租户系统很常见)
  • 按资源:同一 DB 慢查询通道每秒最多 N 次
  • 按组合键(userId + endpoint) / (ip + endpoint) / (tenant + endpoint)

配套策略(非常实用):

  • 拒绝(Fail-Fast):直接 429 / 503
  • 排队(Queue):短队列 + 超时(别无限排队)
  • 降级(Degrade):返回缓存/默认值/简化版本
  • 熔断(Circuit Breaker):这是另一套东西,但经常和限流一起用

1. 固定窗口(Fixed Window Counter)

思路

把时间切成固定窗口(比如 1 秒一格),每个窗口计数,超过阈值就拒绝。

优点

  • 实现简单,性能高
  • 适合粗粒度限流(比如网关层)

缺点(致命点)

  • 窗口边界问题:在窗口末尾和下一个窗口开头可能"瞬间双倍放行"

伪代码

  • key = resource + windowStart
  • incr(key) > limit -> reject

Redis 实现(Lua,原子性)

lua 复制代码
-- KEYS[1] = key
-- ARGV[1] = limit
-- ARGV[2] = expireSeconds
local current = redis.call("INCR", KEYS[1])
if current == 1 then
  redis.call("EXPIRE", KEYS[1], ARGV[2])
end
if current > tonumber(ARGV[1]) then
  return 0
end
return 1

适用

  • 对"边界突刺"不敏感的场景:日志上报、非核心接口、简单防刷

2. 滑动窗口计数(Sliding Window Counter)

思路

仍然基于计数,但用"多个小窗口"近似滑动窗口:例如 1 分钟窗口拆成 60 个 1 秒桶。

窗口内总和 = 最近 60 个桶计数之和。

优点

  • 边界突刺大幅缓解(比固定窗口稳)
  • 实现相对可控

缺点

  • 需要维护多个桶(更多 Redis key / 内存桶)
  • 统计需要 sum(可用 ZSET/Hash 优化)

Redis ZSET 版本(更精确的滑动窗口)

做法 :每个请求写入一个时间戳 score;窗口内通过 ZREMRANGEBYSCORE 清理旧数据 + ZCARD 计数

Lua(高并发下建议用 Lua 保证原子)

lua 复制代码
-- KEYS[1] = zsetKey
-- ARGV[1] = nowMillis
-- ARGV[2] = windowMillis
-- ARGV[3] = limit
-- ARGV[4] = expireSeconds
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local min = now - window

redis.call("ZREMRANGEBYSCORE", KEYS[1], 0, min)
local count = redis.call("ZCARD", KEYS[1])
if count >= tonumber(ARGV[3]) then
  return 0
end
redis.call("ZADD", KEYS[1], now, tostring(now) .. "-" .. tostring(redis.call("INCR", KEYS[1]..":seq")))
redis.call("EXPIRE", KEYS[1], ARGV[4])
redis.call("EXPIRE", KEYS[1]..":seq", ARGV[4])
return 1

适用

  • 防刷、防爆破、短信验证码、登录、发送邮件等强需求"精确窗口"的场景

3. 漏桶(Leaky Bucket)

思路

请求先进入"桶"(队列),桶以固定速率漏水(处理请求)。桶满则拒绝。

关键特征:输出速率恒定

优点

  • 把突发流量"磨平",保护下游
  • 输出稳定,非常适合保护 DB / 依赖服务

缺点

  • 不能很好利用空闲时的余量(输出永远固定)
  • 需要队列/调度器(实现复杂一些)

适用

  • 下游很脆(DB/第三方 API)需要稳定 QPS
  • 例如:写库、发券、下单后置处理

4. 令牌桶(Token Bucket)⭐ 最常用

思路

系统按固定速率往桶里加 token(最多 cap)。请求来消耗 token,有 token 就放行,没有就拒绝(或等待)。

关键特征:允许突发(桶里攒的 token 可以一次性用掉)。

优点

  • 既能控制长期速率,又能允许突发(比漏桶更友好)
  • 工程里最常见:网关、服务端接口、SDK

缺点

  • 分布式实现要处理一致性/时钟/并发(通常用 Redis/Lua)

Redis Lua(令牌桶)

核心参数:

  • rate:每秒产生 token 数
  • cap:桶容量
  • 状态:
    • tokens:当前 token 数(可为小数)
    • ts:上次刷新时间(毫秒)
lua 复制代码
-- KEYS[1] = key
-- ARGV[1] = nowMillis
-- ARGV[2] = ratePerSec
-- ARGV[3] = capacity
-- ARGV[4] = cost (usually 1)
-- ARGV[5] = expireSeconds

local key = KEYS[1]
local now = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local cap = tonumber(ARGV[3])
local cost = tonumber(ARGV[4])

local tokens = tonumber(redis.call("HGET", key, "tokens"))
local ts = tonumber(redis.call("HGET", key, "ts"))

if tokens == nil then tokens = cap end
if ts == nil then ts = now end

local delta = math.max(0, now - ts) / 1000.0
local filled = math.min(cap, tokens + delta * rate)

local allowed = 0
if filled >= cost then
  allowed = 1
  filled = filled - cost
end

redis.call("HSET", key, "tokens", filled, "ts", now)
redis.call("EXPIRE", key, tonumber(ARGV[5]))
return allowed

适用

  • 大部分接口限流都用它:既稳又不浪费

5. 并发限流(Concurrency Limiting)

思路

限制"同时在处理的请求数",而不是 QPS。

实现方式:

  • Semaphore/ThreadPool 直接限并发
  • 或者把慢请求/IO 绑定资源限制住(比如 DB 连接池)

优点

  • 对"慢请求拖垮系统"特别有效
  • 能直接对应资源瓶颈(线程池/连接池/CPU)

缺点

  • 不能直接控制"每秒多少请求"
  • 需要合理的超时/降级,否则会排队堆积

Java 示例(Servlet/Controller 层)

java 复制代码
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class ConcurrencyLimiter {
    private final Semaphore sem;

    public ConcurrencyLimiter(int maxConcurrent) {
        this.sem = new Semaphore(maxConcurrent);
    }

    public boolean tryAcquire(long timeoutMs) throws InterruptedException {
        return sem.tryAcquire(timeoutMs, TimeUnit.MILLISECONDS);
    }

    public void release() {
        sem.release();
    }
}

6. 分布式限流:你绕不开的坑

6.1 单机 vs 分布式

  • 单机:Guava RateLimiter、Bucket4j(内存)
  • 分布式:Redis/Lua、网关(Nginx/Envoy/Kong)、服务网格

6.2 核心坑

  • 原子性:多个实例同时判断+更新,必须原子(Lua / Redis script)
  • 时钟漂移:用毫秒时间戳时要小心(通常"误差可接受",但别跨机器用本地时间做对账)
  • key 爆炸:按 userId/ip/endpoint 组合很容易爆 key,记得加过期和降维
  • 热 key :某一个接口/租户超热,导致 Redis 单点热点(可分片:key:{hashTag}

7. Spring Boot 落地:最实用的三种方式

7.1 AOP + 注解(服务内限流)

适合:单体/少量实例、或配合 Redis 做分布式。

注解:

java 复制代码
import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    String key();          // SpEL 或自定义 key
    long limit();          // 最大次数
    long windowMs() default 1000; // 窗口大小
}

AOP(示意:用 Redis ZSET 滑动窗口 / 或 token bucket Lua):

java 复制代码
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
    String key = "rl:" + rateLimit.key(); // 实际建议用 SpEL 解析 userId/ip/endpoint
    boolean allowed = redisSlidingWindowAllow(key, rateLimit.limit(), rateLimit.windowMs());
    if (!allowed) {
        throw new TooManyRequestsException("Too many requests");
    }
    return pjp.proceed();
}

7.2 网关层限流(推荐)

  • Nginx limit_req(简单粗暴)
  • Spring Cloud Gateway RequestRateLimiter(令牌桶,常搭 Redis)
  • Envoy / Kong / APISIX 也都成熟

网关限流的好处:把坏流量挡在外面,后端不被打爆。

7.3 线程池/队列保护(并发+排队)

  • 对"慢依赖"用并发限流
  • 对"异步任务"用队列长度 + 拒绝策略

8. 算法怎么选:一张不废话的对照表

需求 推荐算法 典型场景
实现最简单 固定窗口 简单防刷、粗限
更平滑,减少边界突刺 滑动窗口 登录/验证码/敏感接口
输出速率必须稳定 漏桶 写库/调用第三方
允许突发但控长期速率 令牌桶 API、网关限流(最常用)
防止慢请求拖垮 并发限流 DB/外部依赖/IO 重接口

9. 真实工程建议(你用得上)

  1. 先定义"失败体验":429?重试提示?还是返回降级数据?
  2. 限流一定要配监控:放行数、拒绝数、等待数、P99 延迟
  3. 给白名单/内部流量开绿灯:比如内部回调、管理员、健康检查
  4. 分层限流:网关挡 80%,服务再挡 20%(更稳)
  5. 别忘了幂等:限流不是幂等的替代品;支付/下单必须幂等
  6. 把 key 设计好tenant:mchNo:api:/path:userId 这种组合要加过期 + 降维策略

10. 附:单机常用库(不分布式)

  • Guava RateLimiter:令牌桶,简单好用(单机)
  • Bucket4j:功能更强(多策略、JCache、Redis 扩展等)
  • Resilience4j:偏容错(限流/隔离/熔断都有),适合微服务

相关推荐
Espresso Macchiato4 天前
Leetcode 3768. Minimum Inversion Count in Subarrays of Fixed Length
滑动窗口·leetcode hard·leetcode双周赛171·leetcode 3768
weixin_461769404 天前
3. 无重复字符的最长子串
c++·算法·滑动窗口·最长字串
nju_spy5 天前
12月力扣每日一题(划分dp + 单调栈 + 堆 + 会议安排)
算法·leetcode·二分查找·动态规划·滑动窗口·单调栈·最大堆
2401_841495647 天前
【LeetCode刷题】爬楼梯
数据结构·python·算法·leetcode·动态规划·滑动窗口·斐波那契数列
tang&14 天前
滑动窗口:双指针的优雅舞步,征服连续区间问题的利器
数据结构·算法·哈希算法·滑动窗口
Tisfy14 天前
LeetCode 3652.按策略买卖股票的最佳时机:滑动窗口
算法·leetcode·题解·滑动窗口
橘子真甜~17 天前
C/C++ Linux网络编程14 - 传输层TCP协议详解(保证可靠传输)
linux·服务器·网络·网络协议·tcp/ip·滑动窗口·拥塞控制
he___H18 天前
滑动窗口一题
java·数据结构·算法·滑动窗口
萌>__<新19 天前
力扣打卡每日一题————最小覆盖子串
数据结构·算法·leetcode·滑动窗口·哈希表