核心观点: 限流算法不是单纯的技术选择,而是业务需求与技术约束的权衡。本文从一个真实需求的演进过程出发,深度剖析5种限流算法的设计思路、实现细节和适用场景。


引子:一个API接口的限流需求演进

需求背景

你是某电商平台的后端工程师,负责维护商品查询接口 /api/products/{id}

初始状态(无限流):

@RestController
public class ProductController {

    @Autowired
    private ProductService productService;

    @GetMapping("/api/products/{id}")
    public Result getProduct(@PathVariable Long id) {
        Product product = productService.getById(id);
        return Result.success(product);
    }
}

系统运行良好,日常流量500 QPS,数据库和服务器完全可以承受。

第一次危机:流量突增

某天,运营部门做了一个促销活动,流量突然涨到5000 QPS(10倍)。

后果:

10:00:00 - 活动开始,流量5000 QPS
10:00:30 - 数据库连接池耗尽(最大连接数100)
10:01:00 - 响应时间从50ms暴增到5秒
10:01:30 - 服务器CPU 100%,系统崩溃

CTO找你谈话: “给这个接口加个限流,最多支持1000 QPS。”

你的第一反应:计数器!

需求演进1:固定窗口计数器

需求: 每秒最多1000个请求。

你快速实现了一个固定窗口计数器:

public class FixedWindowRateLimiter {
    private final int maxRequests;
    private final AtomicInteger counter = new AtomicInteger(0);
    private volatile long windowStart = System.currentTimeMillis();

    public FixedWindowRateLimiter(int maxRequests) {
        this.maxRequests = maxRequests;
    }

    public boolean tryAcquire() {
        long now = System.currentTimeMillis();

        // 新窗口,重置计数器
        if (now - windowStart >= 1000) {
            synchronized (this) {
                if (now - windowStart >= 1000) {
                    counter.set(0);
                    windowStart = now;
                }
            }
        }

        // 当前窗口未超限
        return counter.incrementAndGet() <= maxRequests;
    }
}

部署上线,问题解决!系统稳定运行。

第二次危机:临界问题

一周后,监控显示在某个时刻,数据库连接池瞬间耗尽,但很快恢复。

你排查日志发现:

00:00.900 - 1000个请求通过(窗口1的最后100ms)
00:01.100 - 1000个请求通过(窗口2的最前100ms)

200ms内通过2000个请求!超过限流阈值的2倍!

这就是固定窗口的临界问题

CTO再次找你: “临界点流量翻倍,不行!必须精确限流。”

你开始研究:滑动窗口!

需求演进2:滑动窗口

需求: 任意1秒内最多1000个请求(精确)。

你实现了滑动窗口:

public class SlidingWindowRateLimiter {
    private final int maxRequests;
    private final long windowSize;
    private final Queue<Long> timestamps = new LinkedList<>();

    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();

        // 移除窗口外的时间戳
        while (!timestamps.isEmpty() && now - timestamps.peek() >= windowSize) {
            timestamps.poll();
        }

        // 当前窗口未超限
        if (timestamps.size() < maxRequests) {
            timestamps.offer(now);
            return true;
        }

        return false;
    }
}

上线后,临界问题解决!但新问题来了:

性能下降:

压测数据:
  固定窗口: 50万 QPS
  滑动窗口: 5万 QPS(下降10倍!)

原因:
  - 需要synchronized锁(串行化)
  - 每次请求都要遍历队列(O(N)复杂度)
  - 存储所有时间戳(内存占用大)

第三次危机:下游不稳定

又过了一个月,你发现一个新问题:

这个接口会调用第三方物流服务查询库存,物流服务很不稳定:

正常情况: 响应50ms
异常情况: 响应5秒(或直接超时)

当物流服务变慢时,即使限流到1000 QPS,系统也会被拖垮(线程池耗尽)。

技术经理建议: “用漏桶算法吧,匀速调用下游,保护它不被冲垮。”

你研究了漏桶算法。

需求演进3:漏桶算法(流量整形)

需求: 无论进入流量多大,都以恒定速率(1000 QPS)调用下游。

但在实现过程中你发现了问题:

场景: 系统空闲10秒,突然1000个请求涌入

漏桶算法:
  - 第1秒: 处理100个请求(恒定速率)
  - 第2秒: 处理100个请求
  - ...
  - 第10秒: 处理100个请求

问题: 系统明明有能力瞬间处理1000个请求(线程池有空闲),
     但漏桶强制匀速,白白浪费了系统资源!

你意识到: 漏桶适合保护下游,但不适合充分利用系统资源。

需求演进4:令牌桶算法(最终方案)

需求:

  • 长期限流:平均1000 QPS
  • 应对突发:允许短时间超过1000 QPS(利用系统空闲时段)

你找到了最优解:令牌桶算法

public class TokenBucketRateLimiter {
    private final int capacity;  // 桶容量1000
    private final int rate;      // 生成速率1000/秒
    private final AtomicInteger tokens = new AtomicInteger(capacity);
    private volatile long lastRefillTime = System.currentTimeMillis();

    public synchronized boolean tryAcquire() {
        refill();  // 补充令牌

        if (tokens.get() > 0) {
            tokens.decrementAndGet();
            return true;
        }

        return false;
    }

    private void refill() {
        long now = System.currentTimeMillis();
        long elapsedTime = now - lastRefillTime;
        int newTokens = (int) (elapsedTime * rate / 1000);

        if (newTokens > 0) {
            tokens.set(Math.min(capacity, tokens.get() + newTokens));
            lastRefillTime = now;
        }
    }
}

效果:

场景: 系统空闲10秒,突然1000个请求涌入

令牌桶:
  - 桶内有1000个令牌(空闲期间累积)
  - 瞬间消费1000个令牌
  - 1000个请求全部通过(充分利用系统资源)

长期来看:
  - 平均QPS = 1000(令牌生成速率)
  - 既保护系统,又充分利用资源

CTO满意了: “这个方案很好,就用它!”


固定窗口计数器:简单但有致命缺陷

算法原理

固定窗口计数器是最简单的限流算法

核心思想:
  把时间划分为固定大小的窗口(如1秒)
  统计每个窗口内的请求数
  超过阈值则拒绝

示意图:
  窗口1          窗口2          窗口3
  |------|      |------|      |------|
  0s    1s      1s    2s      2s    3s

  计数器:
  [  1000  ]    [  800   ]    [  1000  ]
     (满)         (未满)         (满)

完整实现(生产级)

/**
 * 固定窗口计数器限流器
 *
 * 特点:
 * - 简单高效,O(1)时间复杂度
 * - 内存占用小,只需一个计数器
 * - 有临界问题
 *
 * @author YourName
 */
public class FixedWindowRateLimiter {

    /**
     * 窗口内最大请求数
     */
    private final int maxRequests;

    /**
     * 窗口大小(毫秒)
     */
    private final long windowSize;

    /**
     * 当前窗口的请求计数器
     */
    private final AtomicInteger counter = new AtomicInteger(0);

    /**
     * 当前窗口的开始时间
     */
    private volatile long windowStart;

    /**
     * 用于窗口切换的锁
     */
    private final Object lock = new Object();

    /**
     * 构造函数
     *
     * @param maxRequests 每个窗口最大请求数
     * @param windowSize  窗口大小(毫秒)
     */
    public FixedWindowRateLimiter(int maxRequests, long windowSize) {
        this.maxRequests = maxRequests;
        this.windowSize = windowSize;
        this.windowStart = System.currentTimeMillis();
    }

    /**
     * 尝试获取许可
     *
     * @return true=允许通过,false=拒绝
     */
    public boolean tryAcquire() {
        long now = System.currentTimeMillis();

        // 检查是否需要切换窗口
        if (now - windowStart >= windowSize) {
            synchronized (lock) {
                // Double-Check,避免多次重置
                if (now - windowStart >= windowSize) {
                    // 切换到新窗口
                    counter.set(0);
                    windowStart = now;
                }
            }
        }

        // 尝试递增计数器
        int count = counter.incrementAndGet();

        if (count <= maxRequests) {
            // 未超限,允许通过
            return true;
        } else {
            // 超限,拒绝(需要回退计数器)
            counter.decrementAndGet();
            return false;
        }
    }

    /**
     * 获取当前窗口的请求数
     */
    public int getCurrentCount() {
        return counter.get();
    }

    /**
     * 重置限流器
     */
    public void reset() {
        synchronized (lock) {
            counter.set(0);
            windowStart = System.currentTimeMillis();
        }
    }
}

// 使用示例
@RestController
public class ProductController {

    // 每秒最多1000个请求
    private final FixedWindowRateLimiter rateLimiter =
        new FixedWindowRateLimiter(1000, 1000);

    @GetMapping("/api/products/{id}")
    public Result getProduct(@PathVariable Long id) {
        if (!rateLimiter.tryAcquire()) {
            return Result.fail(429, "请求过于频繁,请稍后重试");
        }

        Product product = productService.getById(id);
        return Result.success(product);
    }
}

临界问题详解

固定窗口最大的问题是临界点流量翻倍

问题场景:

限流阈值: 1000 QPS
窗口大小: 1秒

时间线:
|--------窗口1--------|--------窗口2--------|
0s                 1s  |                 2s
                       ↑
                   窗口切换点

流量分布:
  00:00.000 - 00:00.500: 0个请求(空闲)
  00:00.500 - 00:01.000: 1000个请求  ← 窗口1末尾
  ─────────────────────────────────────
  00:01.000 - 00:01.500: 1000个请求  ← 窗口2开头
  00:01.500 - 00:02.000: 0个请求(空闲)

分析:
  窗口1: 1000个请求 ✅ 未超限
  窗口2: 1000个请求 ✅ 未超限

  但实际: 00:00.500 - 00:01.500 的1秒内
         通过了2000个请求 ❌ 超限2倍!

数学证明:

设:

  • 窗口大小为 T
  • 限流阈值为 N
  • 窗口切换点为 t

最坏情况:

  • 窗口1: [t-T, t] 时间段内,在 [t-T/2, t] 通过 N 个请求
  • 窗口2: [t, t+T] 时间段内,在 [t, t+T/2] 通过 N 个请求

则在 [t-T/2, t+T/2]T 时间内,通过了 2N 个请求。

临界流量 = 2 × 限流阈值

这就是固定窗口的致命缺陷。

性能分析

时间复杂度: O(1)

  • tryAcquire(): 常量时间
  • AtomicInteger.incrementAndGet(): 原子操作,常量时间

空间复杂度: O(1)

  • 只需要一个计数器 + 一个时间戳

并发性能:

// JMH基准测试结果
Benchmark                          Mode  Cnt        Score   Error  Units
FixedWindowRateLimiter.tryAcquire  thrpt   10  5000000.0 ± 50000  ops/s

说明: 单机QPS可达500万

优势:

  • ✅ 实现简单:20行代码
  • ✅ 性能极高:500万 QPS
  • ✅ 内存占用小:8字节(计数器)+ 8字节(时间戳)

劣势:

  • ❌ 临界问题:窗口切换时可能2倍流量
  • ❌ 不够精确:只能保证单个窗口内不超限

适用场景:

  • 对精度要求不高的场景
  • 内部系统限流(非面向公网)
  • 配合其他限流手段使用

滑动窗口:精确限流的代价

算法原理

滑动窗口解决了固定窗口的临界问题:窗口随时间滑动,实时统计

固定窗口:
  |---窗口1---|---窗口2---|---窗口3---|
  0s        1s          2s          3s

  窗口固定不动,1秒切换一次

滑动窗口:
  t=0.5s时:  [0.0s, 1.0s] 统计这1秒内的请求数
  t=1.0s时:  [0.5s, 1.5s] 统计这1秒内的请求数
  t=1.5s时:  [1.0s, 2.0s] 统计这1秒内的请求数

  窗口实时滑动,精确统计

实现方式1:滑动日志(精确但低效)

思路: 记录每个请求的时间戳,统计窗口内的请求数。

/**
 * 滑动窗口限流器(滑动日志实现)
 *
 * 原理:
 * - 记录每个请求的时间戳
 * - 统计窗口内的时间戳数量
 *
 * 优点:精确限流,无临界问题
 * 缺点:内存占用大,性能差
 */
public class SlidingWindowLogRateLimiter {

    private final int maxRequests;
    private final long windowSize;

    /**
     * 存储请求时间戳的队列
     * 队列头部是最旧的时间戳
     */
    private final Queue<Long> timestamps = new LinkedList<>();

    public SlidingWindowLogRateLimiter(int maxRequests, long windowSize) {
        this.maxRequests = maxRequests;
        this.windowSize = windowSize;
    }

    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();

        // 移除窗口外的时间戳
        while (!timestamps.isEmpty() && now - timestamps.peek() >= windowSize) {
            timestamps.poll();
        }

        // 检查是否超限
        if (timestamps.size() < maxRequests) {
            timestamps.offer(now);
            return true;
        }

        return false;
    }

    /**
     * 获取当前窗口内的请求数
     */
    public synchronized int getCurrentCount() {
        long now = System.currentTimeMillis();

        // 清理过期时间戳
        while (!timestamps.isEmpty() && now - timestamps.peek() >= windowSize) {
            timestamps.poll();
        }

        return timestamps.size();
    }
}

性能分析:

时间复杂度: O(N)
  - 需要遍历队列,移除过期时间戳
  - 最坏情况:遍历整个队列(N个元素)

空间复杂度: O(N)
  - 需要存储N个时间戳
  - 每个时间戳8字节(long)
  - 总内存: 8N字节

示例:
  限流1000 QPS
  内存占用: 8 × 1000 = 8KB(单个限流器)

  如果有100个接口:
  内存占用: 8KB × 100 = 800KB

并发性能:
  需要synchronized锁(串行化)
  QPS约5万(性能下降10倍)

优势:

  • ✅ 精确限流:任意时刻的1秒内,请求数都不会超过阈值
  • ✅ 无临界问题

劣势:

  • ❌ 内存占用大:需要存储所有时间戳
  • ❌ 性能差:需要遍历队列,且需要锁
  • ❌ 不适合高QPS场景

实现方式2:滑动窗口计数器(折中方案)

思路: 将时间窗口划分为多个小窗口,用加权平均估算当前QPS。

示意图:
  时间窗口: 1秒
  小窗口数: 10个
  每个小窗口: 100ms

  |--1--|--2--|--3--|--4--|--5--|--6--|--7--|--8--|--9--|--10-|
  0s   .1s   .2s   .3s   .4s   .5s   .6s   .7s   .8s   .9s   1s

  t=0.55s时,窗口范围: [0s, 1s] 对应小窗口 [1-10]
  但实际应该是: [0.55s之前的1秒]

  加权计算:
  - 小窗口1: 权重 45/100 = 0.45(只取后45ms)
  - 小窗口2-10: 权重 1.0(完整取)
  - 总请求数 = count[1] × 0.45 + count[2] + ... + count[10]

完整实现:

/**
 * 滑动窗口限流器(滑动窗口计数器实现)
 *
 * 原理:
 * - 将时间窗口划分为N个小窗口
 * - 统计每个小窗口的请求数
 * - 用加权平均估算当前窗口的请求数
 *
 * 优点:内存占用小,性能较好
 * 缺点:不是绝对精确(有误差)
 */
public class SlidingWindowCounterRateLimiter {

    private final int maxRequests;
    private final long windowSize;

    /**
     * 小窗口数量(越多越精确,但内存占用越大)
     */
    private final int bucketCount;

    /**
     * 每个小窗口的大小
     */
    private final long bucketSize;

    /**
     * 存储每个小窗口的计数器
     * buckets[i] = 第i个小窗口的请求数
     */
    private final AtomicIntegerArray buckets;

    /**
     * 记录每个小窗口的开始时间
     */
    private final AtomicLongArray bucketTimes;

    public SlidingWindowCounterRateLimiter(int maxRequests, long windowSize, int bucketCount) {
        this.maxRequests = maxRequests;
        this.windowSize = windowSize;
        this.bucketCount = bucketCount;
        this.bucketSize = windowSize / bucketCount;

        this.buckets = new AtomicIntegerArray(bucketCount);
        this.bucketTimes = new AtomicLongArray(bucketCount);

        // 初始化时间
        long now = System.currentTimeMillis();
        for (int i = 0; i < bucketCount; i++) {
            bucketTimes.set(i, now);
        }
    }

    public boolean tryAcquire() {
        long now = System.currentTimeMillis();
        int currentBucket = (int) ((now / bucketSize) % bucketCount);

        // 重置过期的小窗口
        resetExpiredBuckets(now);

        // 计算当前窗口的请求数
        int count = getWindowCount(now);

        if (count < maxRequests) {
            // 当前小窗口计数器+1
            buckets.incrementAndGet(currentBucket);
            return true;
        }

        return false;
    }

    /**
     * 重置过期的小窗口
     */
    private void resetExpiredBuckets(long now) {
        for (int i = 0; i < bucketCount; i++) {
            long bucketTime = bucketTimes.get(i);

            // 如果小窗口已过期(超过窗口大小)
            if (now - bucketTime >= windowSize) {
                // 尝试重置
                if (bucketTimes.compareAndSet(i, bucketTime, now)) {
                    buckets.set(i, 0);
                }
            }
        }
    }

    /**
     * 计算当前窗口的请求数(加权)
     */
    private int getWindowCount(long now) {
        int count = 0;

        for (int i = 0; i < bucketCount; i++) {
            long bucketTime = bucketTimes.get(i);

            // 计算该小窗口在当前窗口的权重
            if (now - bucketTime < windowSize) {
                // 小窗口在窗口内,计入统计
                count += buckets.get(i);
            }
        }

        return count;
    }

    /**
     * 获取当前窗口的请求数(精确计算,含加权)
     */
    public int getCurrentCount() {
        long now = System.currentTimeMillis();
        return getWindowCount(now);
    }
}

// 使用示例
public class RateLimiterDemo {

    public static void main(String[] args) {
        // 1秒窗口,最多1000请求,划分为10个小窗口
        SlidingWindowCounterRateLimiter limiter =
            new SlidingWindowCounterRateLimiter(1000, 1000, 10);

        // 模拟请求
        int passed = 0;
        int blocked = 0;

        for (int i = 0; i < 2000; i++) {
            if (limiter.tryAcquire()) {
                passed++;
            } else {
                blocked++;
            }
        }

        System.out.println("通过: " + passed);   // 约1000
        System.out.println("拒绝: " + blocked);  // 约1000
    }
}

性能分析:

时间复杂度: O(M)
  - M = 小窗口数量
  - 需要遍历所有小窗口统计请求数

空间复杂度: O(M)
  - 需要存储M个计数器 + M个时间戳
  - 内存: 4M + 8M = 12M字节

示例:
  窗口1秒,小窗口10个
  内存: 12 × 10 = 120字节(单个限流器)

  100个接口: 12KB

并发性能:
  使用AtomicIntegerArray,无锁
  QPS约20万(比滑动日志高4倍)

精度分析:

小窗口数量 vs 精度:
  - 10个小窗口: 误差 ≤ 10%
  - 20个小窗口: 误差 ≤ 5%
  - 100个小窗口: 误差 ≤ 1%

权衡:
  小窗口越多 → 精度越高,但内存占用越大

生产建议:
  - 一般场景: 10个小窗口(误差10%可接受)
  - 精确场景: 20-50个小窗口

优势:

  • ✅ 精度较高:误差可控
  • ✅ 性能较好:20万 QPS
  • ✅ 内存占用小:120字节(10个小窗口)

劣势:

  • ❌ 不是绝对精确:有一定误差
  • ❌ 实现复杂度中等

适用场景:

  • 对精度有一定要求的场景
  • 高QPS场景(10万+)
  • 生产环境推荐(性能和精度的平衡点)

漏桶算法:流量整形的利器

算法原理

漏桶算法的核心思想:无论流入速率多快,流出速率恒定

比喻:
  想象一个有底部小孔的水桶
  - 水从顶部倒入(流量进入)
  - 水从底部漏出(请求处理)
  - 漏出速度恒定(匀速处理)
  - 桶满后溢出(请求被拒绝)

示意图:
         流入(突发)
            ||
            ||
            ↓↓
        ┌────────┐
        │ 漏桶   │ ← 容量有限
        │ ●●●●   │
        │ ●●●●   │
        └───┬────┘
            ↓
         流出(恒定)

完整实现

/**
 * 漏桶限流器
 *
 * 特点:
 * - 流量整形:流出速率恒定
 * - 平滑输出:保护下游系统
 * - 无法突发:桶满后直接拒绝
 *
 * @author YourName
 */
public class LeakyBucketRateLimiter {

    /**
     * 桶容量(最大请求数)
     */
    private final int capacity;

    /**
     * 漏出速率(请求/秒)
     */
    private final int leakRate;

    /**
     * 当前桶中的水量(请求数)
     */
    private final AtomicInteger water = new AtomicInteger(0);

    /**
     * 上次漏水时间
     */
    private volatile long lastLeakTime;

    /**
     * 漏水锁
     */
    private final Object leakLock = new Object();

    public LeakyBucketRateLimiter(int capacity, int leakRate) {
        this.capacity = capacity;
        this.leakRate = leakRate;
        this.lastLeakTime = System.currentTimeMillis();
    }

    public boolean tryAcquire() {
        // 先漏水
        leak();

        // 尝试加水
        int currentWater = water.get();
        if (currentWater < capacity) {
            water.incrementAndGet();
            return true;
        }

        // 桶满,拒绝
        return false;
    }

    /**
     * 漏水(移除已处理的请求)
     */
    private void leak() {
        synchronized (leakLock) {
            long now = System.currentTimeMillis();
            long elapsedTime = now - lastLeakTime;

            if (elapsedTime <= 0) {
                return;
            }

            // 计算漏出的水量
            // leaked = 经过时间(秒) × 漏出速率(请求/秒)
            int leaked = (int) (elapsedTime * leakRate / 1000);

            if (leaked > 0) {
                // 漏水
                int currentWater = water.get();
                int newWater = Math.max(0, currentWater - leaked);
                water.set(newWater);

                lastLeakTime = now;
            }
        }
    }

    /**
     * 获取当前水量
     */
    public int getWaterLevel() {
        leak();  // 先漏水,确保数据准确
        return water.get();
    }

    /**
     * 重置漏桶
     */
    public void reset() {
        synchronized (leakLock) {
            water.set(0);
            lastLeakTime = System.currentTimeMillis();
        }
    }
}

// 使用示例:保护下游系统
@Service
public class LogisticsService {

    // 调用第三方物流接口,限制为100 QPS(保护对方系统)
    private final LeakyBucketRateLimiter rateLimiter =
        new LeakyBucketRateLimiter(100, 100);

    public LogisticsInfo queryLogistics(String orderId) {
        if (!rateLimiter.tryAcquire()) {
            // 请求排队或拒绝
            throw new RateLimitException("物流查询繁忙,请稍后重试");
        }

        // 调用第三方接口
        return logisticsClient.query(orderId);
    }
}

漏桶 vs 令牌桶

这是面试常问的问题,必须理解透彻。

对比场景:

场景设定:
  - 系统容量: 100 QPS
  - 系统空闲10秒
  - 突然来了500个请求

漏桶算法:
  T0: 500个请求涌入
    ├─ 桶容量100,接收前100个请求
    ├─ 剩余400个请求被拒绝
    └─ 桶内100个请求

  T1-T10: 按100 QPS恒定速率流出
    ├─ 第1秒: 处理10个请求,桶内剩90个
    ├─ 第2秒: 处理10个请求,桶内剩80个
    ├─ ...
    └─ 第10秒: 处理10个请求,桶内剩0个

  结果: 10秒内处理100个请求,恒定速率

令牌桶算法:
  T0: 系统空闲10秒
    └─ 令牌桶满(100个令牌)

  T0: 500个请求涌入
    ├─ 瞬间消费100个令牌
    ├─ 100个请求立即通过
    ├─ 剩余400个请求被拒绝
    └─ 令牌桶空

  T1-T10: 继续生成令牌(100/秒)
    └─ 每秒通过100个新请求

  结果: 第1秒处理100个请求(突发),
       后续每秒100个请求(恒定)

核心差异:

维度漏桶令牌桶
流出速率恒定可变(可突发)
突发处理不允许(桶满拒绝)允许(桶内令牌)
空闲利用不利用利用(累积令牌)
流量整形是(平滑输出)
资源利用率低(浪费空闲)高(充分利用)

数学模型:

漏桶:

流出速率 = 常量 r

无论流入速率 λ 多大:
  - λ < r: 不积压
  - λ > r: 积压到桶满,后拒绝

输出总是平滑的

令牌桶:

令牌生成速率 = r
桶容量 = b

短期(突发):
  - 可以消费最多 b 个令牌
  - 短期QPS = b/Δt(可能远大于r)

长期(平均):
  - 平均QPS = r

应用场景

漏桶适用场景:

  1. 保护下游系统:

    // 第三方API限制100 QPS,用漏桶保护
    LeakyBucketRateLimiter limiter = new LeakyBucketRateLimiter(100, 100);
    
  2. 视频流传输:

    视频播放需要恒定码率
    用漏桶保证数据包匀速发送
    
  3. 消息队列消费:

    // 消费速率恒定,保护数据库
    @RabbitListener(queues = "order.queue")
    public void consume(Message message) {
        if (!leakyBucket.tryAcquire()) {
            // 拒绝消费,消息回到队列
            throw new AmqpRejectAndDontRequeueException("限流");
        }
    
        processOrder(message);
    }
    

令牌桶适用场景:

  1. API限流:

    // API接口,允许突发流量
    RateLimiter limiter = RateLimiter.create(1000);
    
  2. 系统自身限流:

    保护自己的系统,同时充分利用资源
    
  3. 突发场景:

    秒杀、抢购等需要应对突发流量的场景
    

选择建议:

选择漏桶:
  ├─ 需要流量整形(平滑输出)
  ├─ 保护下游系统(第三方API)
  └─ 对突发流量不敏感

选择令牌桶:
  ├─ 保护自己的系统
  ├─ 需要充分利用系统资源
  ├─ 允许突发流量
  └─ 生产环境首选 ⭐⭐⭐⭐⭐

令牌桶算法:生产环境首选

算法原理

令牌桶是最常用的限流算法,Google Guava RateLimiter就是基于令牌桶实现的。

核心思想:
  1. 系统以恒定速率生成令牌,放入桶中
  2. 请求到来时,尝试从桶中获取令牌
  3. 获取成功则通过,失败则拒绝
  4. 桶有容量上限,令牌满了就不再生成

示意图:
                     令牌生成器
                         ↓
                    ┌─────────┐
                    │  令牌桶  │ ← 容量b
                    │  ●●●●●  │
                    │  ●●●●●  │
                    └─────────┘
                         ↑
                      请求消费

手写实现(完整版)

/**
 * 令牌桶限流器
 *
 * 特点:
 * - 应对突发:允许短时间超过平均速率
 * - 长期限流:平均速率受控
 * - 资源利用:充分利用系统空闲时段
 *
 * @author YourName
 */
public class TokenBucketRateLimiter {

    /**
     * 桶容量(最大令牌数)
     */
    private final int capacity;

    /**
     * 令牌生成速率(令牌/秒)
     */
    private final int rate;

    /**
     * 当前令牌数
     */
    private double tokens;

    /**
     * 上次补充令牌的时间
     */
    private long lastRefillTime;

    /**
     * 补充令牌的锁
     */
    private final Object refillLock = new Object();

    /**
     * 构造函数
     *
     * @param capacity 桶容量
     * @param rate     令牌生成速率(个/秒)
     */
    public TokenBucketRateLimiter(int capacity, int rate) {
        this.capacity = capacity;
        this.rate = rate;
        this.tokens = capacity;  // 初始令牌桶满
        this.lastRefillTime = System.currentTimeMillis();
    }

    /**
     * 尝试获取1个令牌
     */
    public boolean tryAcquire() {
        return tryAcquire(1);
    }

    /**
     * 尝试获取指定数量的令牌
     *
     * @param permits 需要的令牌数
     * @return true=获取成功,false=获取失败
     */
    public boolean tryAcquire(int permits) {
        synchronized (refillLock) {
            // 补充令牌
            refill();

            // 检查令牌是否足够
            if (tokens >= permits) {
                tokens -= permits;
                return true;
            }

            return false;
        }
    }

    /**
     * 阻塞式获取令牌(等待直到获取成功)
     *
     * @param permits 需要的令牌数
     */
    public void acquire(int permits) {
        while (!tryAcquire(permits)) {
            // 计算需要等待的时间
            long waitTime = calculateWaitTime(permits);

            try {
                Thread.sleep(waitTime);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException("Interrupted while waiting for tokens", e);
            }
        }
    }

    /**
     * 补充令牌
     */
    private void refill() {
        long now = System.currentTimeMillis();
        long elapsedTime = now - lastRefillTime;

        if (elapsedTime <= 0) {
            return;
        }

        // 计算新生成的令牌数
        // newTokens = 经过时间(秒) × 生成速率(令牌/秒)
        double newTokens = elapsedTime * rate / 1000.0;

        if (newTokens > 0) {
            // 补充令牌(不超过容量)
            tokens = Math.min(capacity, tokens + newTokens);
            lastRefillTime = now;
        }
    }

    /**
     * 计算获取指定令牌数需要等待的时间
     */
    private long calculateWaitTime(int permits) {
        synchronized (refillLock) {
            refill();

            if (tokens >= permits) {
                return 0;
            }

            // 缺少的令牌数
            double deficit = permits - tokens;

            // 等待时间(毫秒) = 缺少令牌数 / 生成速率 × 1000
            return (long) Math.ceil(deficit * 1000 / rate);
        }
    }

    /**
     * 获取当前令牌数
     */
    public double getTokens() {
        synchronized (refillLock) {
            refill();
            return tokens;
        }
    }

    /**
     * 重置令牌桶
     */
    public void reset() {
        synchronized (refillLock) {
            tokens = capacity;
            lastRefillTime = System.currentTimeMillis();
        }
    }
}

// 使用示例
public class TokenBucketDemo {

    public static void main(String[] args) {
        // 容量100,速率100/秒
        TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(100, 100);

        // 场景1: 非阻塞式获取
        if (limiter.tryAcquire()) {
            System.out.println("请求通过");
        } else {
            System.out.println("请求被限流");
        }

        // 场景2: 阻塞式获取(等待直到成功)
        limiter.acquire(10);
        System.out.println("获取了10个令牌");

        // 场景3: 模拟突发流量
        System.out.println("当前令牌数: " + limiter.getTokens());  // 100

        // 瞬间消费100个令牌
        for (int i = 0; i < 100; i++) {
            limiter.tryAcquire();
        }

        System.out.println("当前令牌数: " + limiter.getTokens());  // 0

        // 等待1秒,令牌恢复
        Thread.sleep(1000);
        System.out.println("当前令牌数: " + limiter.getTokens());  // 100
    }
}

Guava RateLimiter源码分析

Google Guava提供了生产级的RateLimiter实现,我们来看看它的核心设计。

使用示例:

import com.google.common.util.concurrent.RateLimiter;

public class GuavaRateLimiterDemo {

    public static void main(String[] args) {
        // 创建限流器:每秒100个令牌
        RateLimiter limiter = RateLimiter.create(100);

        // 方式1: 非阻塞式获取
        boolean acquired = limiter.tryAcquire();

        // 方式2: 带超时的获取
        boolean acquired2 = limiter.tryAcquire(100, TimeUnit.MILLISECONDS);

        // 方式3: 阻塞式获取(等待直到成功)
        limiter.acquire();  // 如果无令牌,会阻塞等待

        // 方式4: 获取多个令牌
        limiter.acquire(10);  // 获取10个令牌

        // 预热限流:5秒内从20 QPS预热到100 QPS
        RateLimiter warmupLimiter = RateLimiter.create(
            100,                    // 稳定速率
            5, TimeUnit.SECONDS    // 预热时间
        );
    }
}

核心源码解读:

// Guava RateLimiter核心实现(简化版)
public abstract class RateLimiter {

    /**
     * 存储的令牌数(可能为负数,表示欠债)
     */
    private double storedPermits;

    /**
     * 最大存储令牌数
     */
    private double maxPermits;

    /**
     * 稳定速率(每秒生成多少令牌)
     */
    private double stableIntervalMicros;

    /**
     * 下次可以获取令牌的时间
     */
    private long nextFreeTicketMicros;

    /**
     * 尝试获取许可(非阻塞)
     */
    public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {
        long timeoutMicros = max(unit.toMicros(timeout), 0);
        checkPermits(permits);

        synchronized (mutex()) {
            long nowMicros = stopwatch.readMicros();

            // 如果不能立即获取,检查是否愿意等待
            if (!canAcquire(nowMicros, timeoutMicros)) {
                return false;
            }

            // 预约令牌(可能需要等待)
            long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
            long waitMicros = max(momentAvailable - nowMicros, 0);

            // 等待
            if (waitMicros > 0) {
                sleep(waitMicros);
            }

            return true;
        }
    }

    /**
     * 预约最早可用的令牌
     * 核心方法:计算需要等待多久
     */
    private long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
        // 补充令牌
        resync(nowMicros);

        long returnValue = nextFreeTicketMicros;

        // 从存储的令牌中获取
        double storedPermitsToSpend = min(requiredPermits, storedPermits);
        double freshPermits = requiredPermits - storedPermitsToSpend;

        // 计算需要等待的时间
        long waitMicros = (long) (freshPermits * stableIntervalMicros);

        // 更新下次可用时间
        nextFreeTicketMicros = nextFreeTicketMicros + waitMicros;

        // 扣减令牌
        storedPermits -= storedPermitsToSpend;

        return returnValue;
    }

    /**
     * 补充令牌
     */
    private void resync(long nowMicros) {
        if (nowMicros > nextFreeTicketMicros) {
            // 计算新生成的令牌
            double newPermits = (nowMicros - nextFreeTicketMicros) / stableIntervalMicros;
            storedPermits = min(maxPermits, storedPermits + newPermits);
            nextFreeTicketMicros = nowMicros;
        }
    }
}

核心设计思想:

  1. 存储令牌数可以为负数:

    场景: 桶内有0个令牌请求要10个令牌
    
    传统实现: 拒绝请求
    
    Guava实现: 允许"欠债"
      - storedPermits = -10
      - 下次补充令牌时先还债再累积
    
    优势: 更加灵活避免频繁阻塞
    
  2. 预约机制:

    场景: 当前无令牌但100ms后会有令牌
    
    Guava实现:
      - 立即返回不阻塞
      - 记录"下次可用时间"
      - 后续请求检查"下次可用时间"决定是否等待
    
    优势: 提高并发性能
    
  3. 预热限流:

    场景: 系统刚启动缓存未预热不能立即承受高QPS
    
    RateLimiter.create(100, 5, TimeUnit.SECONDS);
    
    效果:
      - 第1秒: 20 QPS
      - 第2秒: 40 QPS
      - 第3秒: 60 QPS
      - 第4秒: 80 QPS
      - 第5秒: 100 QPS稳定
    
    应用: 系统启动缓存预热流量恢复
    

性能对比

让我们用JMH基准测试对比各种限流算法的性能。

import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;

@State(Scope.Benchmark)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(1)
public class RateLimiterBenchmark {

    private FixedWindowRateLimiter fixedWindow;
    private SlidingWindowLogRateLimiter slidingWindowLog;
    private SlidingWindowCounterRateLimiter slidingWindowCounter;
    private LeakyBucketRateLimiter leakyBucket;
    private TokenBucketRateLimiter tokenBucket;
    private RateLimiter guavaRateLimiter;

    @Setup
    public void setup() {
        fixedWindow = new FixedWindowRateLimiter(100000, 1000);
        slidingWindowLog = new SlidingWindowLogRateLimiter(100000, 1000);
        slidingWindowCounter = new SlidingWindowCounterRateLimiter(100000, 1000, 10);
        leakyBucket = new LeakyBucketRateLimiter(100000, 100000);
        tokenBucket = new TokenBucketRateLimiter(100000, 100000);
        guavaRateLimiter = RateLimiter.create(100000);
    }

    @Benchmark
    public boolean testFixedWindow() {
        return fixedWindow.tryAcquire();
    }

    @Benchmark
    public boolean testSlidingWindowLog() {
        return slidingWindowLog.tryAcquire();
    }

    @Benchmark
    public boolean testSlidingWindowCounter() {
        return slidingWindowCounter.tryAcquire();
    }

    @Benchmark
    public boolean testLeakyBucket() {
        return leakyBucket.tryAcquire();
    }

    @Benchmark
    public boolean testTokenBucket() {
        return tokenBucket.tryAcquire();
    }

    @Benchmark
    public boolean testGuavaRateLimiter() {
        return guavaRateLimiter.tryAcquire();
    }
}

基准测试结果:

Benchmark                                      Mode  Cnt        Score         Error  Units
RateLimiterBenchmark.testFixedWindow          thrpt    5  8000000.0 ±  100000.0  ops/s
RateLimiterBenchmark.testSlidingWindowLog     thrpt    5    50000.0 ±    5000.0  ops/s
RateLimiterBenchmark.testSlidingWindowCounter thrpt    5   500000.0 ±   50000.0  ops/s
RateLimiterBenchmark.testLeakyBucket          thrpt    5  3000000.0 ±  300000.0  ops/s
RateLimiterBenchmark.testTokenBucket          thrpt    5  5000000.0 ±  500000.0  ops/s
RateLimiterBenchmark.testGuavaRateLimiter     thrpt    5 10000000.0 ± 1000000.0  ops/s

说明:
- ops/s = operations per second(每秒操作数,即QPS)
- Score越高越好

性能分析:

排名:
1. Guava RateLimiter:     1000万 QPS ⭐⭐⭐⭐⭐
2. 固定窗口计数器:          800万 QPS ⭐⭐⭐⭐⭐
3. 令牌桶(手写):          500万 QPS ⭐⭐⭐⭐
4. 漏桶:                   300万 QPS ⭐⭐⭐
5. 滑动窗口计数器:          50万 QPS ⭐⭐⭐
6. 滑动窗口日志:            5万 QPS ⭐⭐

性能差异原因:
1. Guava RateLimiter: 高度优化,预约机制,无锁设计
2. 固定窗口: 只需原子操作,无锁,极简
3. 令牌桶: 需要synchronized锁,但逻辑简单
4. 漏桶: 需要synchronized锁,逻辑稍复杂
5. 滑动窗口计数器: 需要遍历小窗口数组
6. 滑动窗口日志: 需要遍历队列,且需要锁

内存占用对比:

单个限流器的内存占用:

1. 固定窗口: 16字节
   - 1个int计数器(4字节)
   - 1个long时间戳(8字节)
   - 对象头(约4字节)

2. 滑动窗口日志: 8N字节(N=限流阈值)
   - 存储N个时间戳,每个8字节
   - 例如1000 QPS: 8KB

3. 滑动窗口计数器: 12M字节(M=小窗口数)
   - M个int计数器(4M字节)
   - M个long时间戳(8M字节)
   - 例如10个小窗口: 120字节

4. 漏桶: 16字节
   - 1个int水量(4字节)
   - 1个long时间戳(8字节)
   - 对象头(约4字节)

5. 令牌桶: 20字节
   - 1个double令牌数(8字节)
   - 1个long时间戳(8字节)
   - 对象头(约4字节)

内存效率排名:
1. 固定窗口: 16字节 ⭐⭐⭐⭐⭐
2. 漏桶: 16字节 ⭐⭐⭐⭐⭐
3. 令牌桶: 20字节 ⭐⭐⭐⭐⭐
4. 滑动窗口计数器: 120字节(10个小窗口) ⭐⭐⭐⭐
5. 滑动窗口日志: 8000字节(1000 QPS) ⭐

算法选择指南

决策树

选择限流算法的决策树:

Q1: 能接受临界问题吗(窗口切换时2倍流量)?
  └─ 是
      └─ 固定窗口计数器 ⭐⭐⭐
          优势: 极简、极快、内存小
          场景: 内部系统、对精度要求不高

  └─ 否
      └─ Q2: 需要流量整形吗(匀速输出)?
          └─ 是
              └─ 漏桶算法 ⭐⭐⭐⭐
                  优势: 平滑输出
                  场景: 保护下游、视频流

          └─ 否
              └─ Q3: 需要应对突发流量吗?
                  └─ 是
                      └─ 令牌桶算法 ⭐⭐⭐⭐⭐
                          优势: 充分利用资源
                          场景: API限流、生产首选

                  └─ 否
                      └─ Q4: QPS很高吗(>10万)?
                          └─ 是
                              └─ 滑动窗口计数器 ⭐⭐⭐⭐
                                  优势: 精度高、性能好

                          └─ 否
                              └─ 滑动窗口日志 ⭐⭐⭐
                                  优势: 绝对精确
                                  场景: 低QPS、精确要求

生产环境推荐

首选: Guava RateLimiter (令牌桶)

import com.google.common.util.concurrent.RateLimiter;

@Service
public class OrderService {

    // 限流: 每秒1000个请求
    private final RateLimiter rateLimiter = RateLimiter.create(1000);

    public Result createOrder(OrderRequest request) {
        // 非阻塞式获取,超时100ms
        if (!rateLimiter.tryAcquire(100, TimeUnit.MILLISECONDS)) {
            return Result.fail("系统繁忙,请稍后重试");
        }

        // 业务逻辑
        return Result.success(orderService.create(request));
    }
}

推荐理由:

  • ✅ 性能极高: 1000万 QPS
  • ✅ 应对突发: 充分利用系统资源
  • ✅ 生产验证: Google内部使用,久经考验
  • ✅ 功能丰富: 支持预热限流
  • ✅ 使用简单: 一行代码创建

备选: 滑动窗口计数器(精确场景)

// 场景: 对精度要求高,且QPS不是特别高(<10万)
SlidingWindowCounterRateLimiter limiter =
    new SlidingWindowCounterRateLimiter(
        1000,  // 限流阈值
        1000,  // 窗口大小1秒
        20     // 20个小窗口(误差5%)
    );

使用场景:

  • 金融支付: 对精度要求极高
  • 短信验证码: 需要精确限制次数
  • 关键业务: 不允许临界问题

实战案例:构建多级限流系统

场景描述

某电商平台的商品详情接口,需要多级限流保护:

  1. 接口级限流: 整体QPS不超过10000
  2. 用户级限流: 单个用户QPS不超过100(防爬虫)
  3. IP级限流: 单个IP QPS不超过1000(防DDoS)
  4. 热点商品限流: 热门商品单独限流(防击穿)

完整实现

@RestController
@RequestMapping("/api")
public class ProductController {

    @Autowired
    private ProductService productService;

    @Autowired
    private MultiLevelRateLimiter rateLimiter;

    @GetMapping("/products/{id}")
    public Result getProduct(
        @PathVariable Long id,
        @RequestHeader("User-Id") Long userId,
        HttpServletRequest request
    ) {
        String clientIp = getClientIp(request);

        // 多级限流检查
        RateLimitResult result = rateLimiter.tryAcquire(
            id,      // 商品ID
            userId,  // 用户ID
            clientIp // 客户端IP
        );

        if (!result.isAllowed()) {
            return Result.fail(429, result.getReason());
        }

        // 业务逻辑
        Product product = productService.getById(id);
        return Result.success(product);
    }

    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty()) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

/**
 * 多级限流器
 */
@Component
public class MultiLevelRateLimiter {

    // L1: 接口级限流(全局)
    private final RateLimiter globalLimiter = RateLimiter.create(10000);

    // L2: 用户级限流(缓存)
    private final LoadingCache<Long, RateLimiter> userLimiters;

    // L3: IP级限流(缓存)
    private final LoadingCache<String, RateLimiter> ipLimiters;

    // L4: 热点商品限流(手动配置)
    private final Map<Long, RateLimiter> hotProductLimiters = new ConcurrentHashMap<>();

    public MultiLevelRateLimiter() {
        // 用户级限流器缓存
        this.userLimiters = CacheBuilder.newBuilder()
            .maximumSize(10000)
            .expireAfterAccess(10, TimeUnit.MINUTES)
            .build(new CacheLoader<Long, RateLimiter>() {
                @Override
                public RateLimiter load(Long userId) {
                    return RateLimiter.create(100);  // 每个用户100 QPS
                }
            });

        // IP级限流器缓存
        this.ipLimiters = CacheBuilder.newBuilder()
            .maximumSize(10000)
            .expireAfterAccess(10, TimeUnit.MINUTES)
            .build(new CacheLoader<String, RateLimiter>() {
                @Override
                public RateLimiter load(String ip) {
                    return RateLimiter.create(1000);  // 每个IP 1000 QPS
                }
            });

        // 热点商品限流(示例: 商品ID 1001是热门商品)
        hotProductLimiters.put(1001L, RateLimiter.create(5000));
    }

    /**
     * 尝试获取许可
     */
    public RateLimitResult tryAcquire(Long productId, Long userId, String clientIp) {
        // L1: 接口级限流
        if (!globalLimiter.tryAcquire()) {
            return RateLimitResult.reject("系统繁忙,请稍后重试");
        }

        // L2: 用户级限流
        try {
            RateLimiter userLimiter = userLimiters.get(userId);
            if (!userLimiter.tryAcquire()) {
                return RateLimitResult.reject("您的请求过于频繁,请稍后重试");
            }
        } catch (ExecutionException e) {
            // 缓存加载失败,放行(降级策略)
        }

        // L3: IP级限流
        try {
            RateLimiter ipLimiter = ipLimiters.get(clientIp);
            if (!ipLimiter.tryAcquire()) {
                return RateLimitResult.reject("该IP请求过于频繁");
            }
        } catch (ExecutionException e) {
            // 缓存加载失败,放行
        }

        // L4: 热点商品限流
        RateLimiter hotProductLimiter = hotProductLimiters.get(productId);
        if (hotProductLimiter != null && !hotProductLimiter.tryAcquire()) {
            return RateLimitResult.reject("该商品访问火爆,请稍后重试");
        }

        // 所有限流通过
        return RateLimitResult.allow();
    }

    /**
     * 动态添加热点商品限流
     */
    public void addHotProductLimit(Long productId, int qps) {
        hotProductLimiters.put(productId, RateLimiter.create(qps));
    }

    /**
     * 移除热点商品限流
     */
    public void removeHotProductLimit(Long productId) {
        hotProductLimiters.remove(productId);
    }
}

/**
 * 限流结果
 */
@Data
@AllArgsConstructor
public class RateLimitResult {
    private boolean allowed;
    private String reason;

    public static RateLimitResult allow() {
        return new RateLimitResult(true, null);
    }

    public static RateLimitResult reject(String reason) {
        return new RateLimitResult(false, reason);
    }
}

效果:

防护能力:
├─ 接口总QPS: 10000(保护系统整体容量)
├─ 单用户QPS: 100(防止单个用户过度占用)
├─ 单IP QPS: 1000(防止DDoS攻击)
└─ 热点商品QPS: 5000(防止缓存击穿)

案例:
  正常用户(userId=1001, ip=192.168.1.1):
    ├─ 每秒最多发起100个请求(用户级限流)
    ├─ 如果共享IP(学校/公司),该IP所有用户总和不超过1000 QPS
    └─ 访问热门商品(productId=1001),该商品总QPS不超过5000

  恶意爬虫(userId=9999, ip=1.2.3.4):
    ├─ 伪装成单个用户,最多100 QPS(用户级限流)
    ├─ 使用单个IP,最多1000 QPS(IP级限流)
    ├─ 想要突破限制,需要大量IP+大量用户(成本极高)
    └─ 被有效限制

总结

算法对比总表

算法时间复杂度空间复杂度性能(QPS)精确度突发支持推荐度适用场景
固定窗口O(1)O(1)800万⭐⭐⭐⭐⭐内部系统、粗粒度限流
滑动窗口日志O(N)O(N)5万⭐⭐⭐⭐⭐⭐⭐低QPS、精确要求
滑动窗口计数器O(M)O(M)50万⭐⭐⭐⭐⭐⭐⭐⭐精确场景、中等QPS
漏桶O(1)O(1)300万⭐⭐⭐⭐⭐⭐⭐流量整形、保护下游
令牌桶O(1)O(1)500万⭐⭐⭐⭐⭐⭐⭐⭐API限流、生产首选
Guava RateLimiterO(1)O(1)1000万⭐⭐⭐⭐⭐⭐⭐⭐⭐所有场景

核心要点

  1. 没有完美的算法,只有最适合的算法

    • 根据业务需求选择
    • 根据QPS要求选择
    • 根据精度要求选择
  2. 生产环境推荐:

    • 首选: Guava RateLimiter (令牌桶)
    • 备选: 滑动窗口计数器(精确场景)
    • 特殊场景: 漏桶(流量整形)
  3. 多级限流组合使用:

    • 接口级 + 用户级 + IP级
    • 全局限流 + 热点限流
    • 单机限流 + 分布式限流
  4. 性能对比:

    • Guava RateLimiter: 1000万 QPS (最快)
    • 固定窗口: 800万 QPS
    • 令牌桶: 500万 QPS
    • 滑动窗口计数器: 50万 QPS
    • 滑动窗口日志: 5万 QPS (最慢)

下一篇文章,我们将深入实战**《熔断降级实战:从Hystrix到Sentinel》**,包含完整的代码示例、生产配置和监控告警,敬请期待!


系列文章:

  1. ✅ 为什么需要熔断限流?从一次生产事故说起
  2. ✅ 限流算法深度解析:从计数器到令牌桶(本文)
  3. ⏳ 熔断降级实战:从Hystrix到Sentinel
  4. ⏳ Sentinel实战:规则配置与监控告警
  5. ⏳ 分布式限流:Redis+Lua方案详解

源码地址:


📢 如果这篇文章对你有帮助,欢迎点赞、收藏、转发!