从固定窗口到令牌桶:一文掌握四种经典限流算法的核心逻辑与 Java 代码实现

一、限流的定义

  • 核心目的:控制请求速率,防止系统因高并发或大流量过载,保障稳定性。
  • 应用场景
    • 防止 DoS 攻击、限制 Web 爬虫。
    • 保护服务器、数据库等资源,避免因突发流量崩溃。
  • 平衡挑战:限流可能拒绝部分请求,影响用户体验,需在系统稳定与用户体验间权衡。

二、四种经典限流算法

1. 固定窗口算法
  • 原理
    将时间划分为固定窗口(如 1 秒),统计窗口内请求次数,超过阈值则拒绝后续请求,窗口结束后计数器清零。
  • 问题
    临界突刺:窗口切换时可能出现双倍流量(如 0.9 秒和 1.1 秒各触发一次阈值请求,实际 1 秒内处理双倍请求)。
  • Java 实现

    java

    class FixedWindowRateLimiter {
        // 窗口内允许的最大请求数
        private final int limit;
        // 窗口大小(毫秒)
        private final long windowSizeMs;
        // 当前窗口内的请求计数器(原子操作保证线程安全)
        private AtomicInteger count = new AtomicInteger(0);
        // 当前窗口的开始时间(原子操作保证线程安全)
        private AtomicLong windowStart = new AtomicLong(System.currentTimeMillis());
    
        /**
         * 构造函数
         * @param limit 窗口内允许的最大请求数
         * @param windowSizeMs 窗口大小(毫秒)
         */
        public FixedWindowRateLimiter(int limit, long windowSizeMs) {
            this.limit = limit;
            this.windowSizeMs = windowSizeMs;
        }
    
        /**
         * 尝试获取一个请求许可
         * @return true表示允许请求,false表示拒绝请求
         */
        public boolean tryAcquire() {
            long now = System.currentTimeMillis();
            // 检查是否需要重置窗口(当前时间已超过窗口结束时间)
            if(now - windowStart.get() > windowSizeMs) {
                // 使用CAS操作原子性地重置窗口开始时间和计数器
                if(windowStart.compareAndSet(windowStart.get(), now)) {
                    count.set(0);
                }
            }
            // 原子性地增加计数并检查是否超过限制
            return count.incrementAndGet() <= limit;
        }
    }
  • 特点
    • 实现简单,内存占用低。
    • 精度低,无法应对临界突刺。
2. 滑动窗口算法
  • 原理
    将固定窗口划分为多个子窗口(如 1 秒分为 5 个 0.2 秒的子窗口),每个子窗口独立计数,窗口随时间滑动时累加有效子窗口的请求数。
  • 优势
    解决固定窗口的临界突刺问题,子窗口越多,统计越精确。
  • Java 实现

    java

    class SlidingWindowRateLimiter {
        // 窗口内允许的最大请求数
        private final int limit;
        // 子窗口数量
        private final int windowCount;
        // 每个子窗口的大小(毫秒)
        private final long windowSizeMs;
        // 子窗口计数器数组(原子操作保证线程安全)
        private final AtomicInteger[] windows;
        // 当前窗口开始时间(原子操作保证线程安全)
        private AtomicLong currentWindowStart = new AtomicLong(System.currentTimeMillis());
        // 当前窗口索引
        private int currentWindowIndex = 0;
    
        /**
         * 构造函数
         * @param limit 窗口内允许的最大请求数
         * @param windowSizeMs 总窗口大小(毫秒)
         * @param windowCount 子窗口数量
         */
        public SlidingWindowRateLimiter(int limit, long windowSizeMs, int windowCount) {
            this.limit = limit;
            this.windowCount = windowCount;
            this.windowSizeMs = windowSizeMs / windowCount; // 计算每个子窗口大小
            this.windows = new AtomicInteger[windowCount];
            for (int i = 0; i < windowCount; i++) {
                windows[i] = new AtomicInteger(0); // 初始化所有子窗口计数器
            }
        }
    
        /**
         * 尝试获取一个请求许可
         * @return true表示允许请求,false表示拒绝请求
         */
        public boolean tryAcquire() {
            long now = System.currentTimeMillis();
            // 计算当前窗口索引
            long elapsed = now - currentWindowStart.get();
            int windowIndex = (int) ((elapsed / windowSizeMs) % windowCount);
            
            // 重置过期窗口
            if (elapsed > windowSizeMs * windowCount) {
                resetAllWindows(); // 如果超过整个窗口周期,重置所有窗口
                currentWindowStart.set(now);
            } else if (elapsed > windowSizeMs) {
                // 重置已过期的子窗口
                int skipCount = (int) (elapsed / windowSizeMs);
                for (int i = 0; i < skipCount; i++) {
                    int idx = (currentWindowIndex + i + 1) % windowCount;
                    windows[idx].set(0);
                }
                currentWindowStart.addAndGet(skipCount * windowSizeMs);
            }
            
            currentWindowIndex = windowIndex;
            // 统计所有子窗口的请求总数
            int total = 0;
            for (AtomicInteger window : windows) {
                total += window.get();
            }
            
            // 检查是否超过限制
            if (total >= limit) {
                return false;
            }
            // 原子性地增加当前子窗口计数
            return windows[windowIndex].incrementAndGet() <= limit;
        }
    
        /**
         * 重置所有子窗口计数器
         */
        private void resetAllWindows() {
            for (AtomicInteger window : windows) {
                window.set(0);
            }
        }
    }
  • 特点
    • 精度高,临界流量控制更严格。
    • 内存占用较高(需维护多个子窗口计数器)。
3. 漏桶算法
  • 原理
    请求如 “水流” 进入漏桶,漏桶以固定速率(如每秒处理 100 请求)“漏水”(处理请求),桶满时拒绝新请求。
  • 特点
    • 平滑处理请求,无突发流量,但无法应对瞬时高峰。
    • 适合需要严格控制平均速率的场景(如消息队列削峰)。
  • Java 实现

    java

    class LeakyBucketRateLimiter {
        // 桶的总容量(最大水量)
        private final int capacity;
        // 漏出速率(每秒漏出多少单位水)
        private final int rate;
        // 当前桶中的水量
        private int water;
        // 上次漏水的时间戳(毫秒)
        private long lastLeakTime;
    
        /**
         * 构造函数
         * @param capacity 桶的总容量
         * @param rate 漏出速率(单位/秒)
         */
        public LeakyBucketRateLimiter(int capacity, int rate) {
            this.capacity = capacity;
            this.rate = rate;
            this.water = 0; // 初始时桶是空的
            this.lastLeakTime = System.currentTimeMillis();
        }
    
        /**
         * 尝试获取一个请求许可(线程安全)
         * @return true表示允许请求,false表示拒绝请求
         */
        public synchronized boolean tryAcquire() {
            long now = System.currentTimeMillis();
            // 计算自上次漏水后经过的时间(毫秒)
            long elapsedMillis = now - lastLeakTime;
            // 计算这段时间应该漏出的水量
            int leakedWater = (int) (elapsedMillis * rate / 1000);
            // 漏水(水量不能为负)
            water = Math.max(0, water - leakedWater);
            lastLeakTime = now; // 更新最后漏水时间
            if (water < capacity) {
                water++; // 增加水量
                return true; // 允许请求通过
            }
            return false; // 拒绝请求
        }
    
    }
4. 令牌桶算法
  • 原理
    系统以固定速率(如每秒生成 100 令牌)向桶中放令牌,桶满则丢弃多余令牌。请求需获取令牌才能处理,无令牌则拒绝。
  • 优势
    • 允许突发流量(桶中预存令牌可应对瞬时高峰)。
    • 长期维持平均速率,兼顾稳定性与灵活性。
  • Java 实现

    java

    class TokenBucketRateLimiter{
        // 桶的总容量(最大令牌数)
        private final int capacity;
        // 令牌填充速率(每秒填充多少个令牌)
        private final double rate;
        // 当前桶中的令牌数量
        private double tokens;
        // 上次补充令牌的时间戳(毫秒)
        private long lastRefillTime;
    
        /**
         * 构造函数
         * @param capacity 桶的总容量
         * @param rate 令牌填充速率(令牌/秒)
         */
        public TokenBucketRateLimiter(int capacity,double rate){
            this.capacity = capacity;
            this.rate = rate;
            this.tokens = capacity; // 初始化时桶是满的
            this.lastRefillTime = System.currentTimeMillis(); // 记录初始时间
        }
    
        /**
         * 尝试获取一个令牌
         * @return true表示获取成功,false表示被限流
         */
        public synchronized boolean tryAcquire(){
            long now = System.currentTimeMillis();
            // 计算自上次补充后经过的时间(秒)
            double elapsedSeconds = (now - lastRefillTime) / 1000.0; // elapsed 时间过去
            // 计算这段时间应该补充的令牌数
            double newTokens = elapsedSeconds * rate;
            // 补充令牌(不超过桶容量)
            tokens = Math.min(capacity, tokens + newTokens);
            lastRefillTime = now; // 更新最后补充时间
    
            // 检查是否有足够令牌
            if(tokens >= 1){
                tokens--; // 消耗一个令牌
                return true; // 允许请求通过
            }
            return false; // 拒绝请求
        }
    }
  • 应用
    Guava 的RateLimiter组件即基于此算法实现。

三、算法对比

算法是否允许突发流量速率控制临界问题典型场景
固定窗口粗略控制平均速率存在突刺简单限流(如测试环境)
滑动窗口精确控制平均速率金融交易、API 网关
漏桶算法严格固定速率消息队列、实时数据处理
令牌桶算法是(依赖桶容量)平均速率 + 突发容忍电商秒杀、广告投放系统

四、总结

  • 固定窗口滑动窗口属于时间窗口类算法,核心差异在于统计精度。
  • 漏桶令牌桶属于流量整形算法,前者限制流出速率,后者允许突发但控制平均速率。
  • 实际选择需结合业务场景:
    • 若需严格控制瞬时流量,选滑动窗口漏桶
    • 若需兼顾突发流量和稳定性,选令牌桶(如 Guava 组件)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值