限流器

概念

在当前的微服务或分布式系统下,需要保证整个系统的高可用,限流就是高可用的实现手段之一,限流的意思是流量限速,当请求到来的速度大于系统处理的速度时,如果积压的请求数量超过阈值,会触发限流策略,后续的请求会被拒绝或排队。限流是出于安全性考虑,避免流量过大或恶意流量将系统打崩。

限流方式

限流的方式大致可以分为本地限流和分布式限流:

  • 本地限流:每个业务接口内部单独设置限流逻辑,每个接口独立控制流量,适合节点粒度的流量控制
  • 分布式限流:分布式限流不同于单机限流,是控制整个服务的流量。限流逻辑一般位于网关或者共享中间件(Redis)中,适合全局流量控制
单机限流 分布式限流
image image

一般是通过网关做分布式限流,网关本身起到请求转发的作用,是流量的统一入口,所以将限流或负载均衡这类共享的非业务逻辑放在网关最为合适。

限流算法

固定窗口限流

固定窗口限流
image

固定窗口限流算法将时间划分为固定的窗口大小(1s),在一个时间窗口内,每到来一个请求,计数器+1,当超过限流阈值后,窗口内后续的请求全部丢弃,直到下一个窗口计数器重置为0。

固定窗口限流算法的优点是实现非常简单,且内存占用极小,只需要存储当前的时间窗口标识以及计数器,缺点是限流不够平滑,并且存在临界限流失效问题:窗口切换时可能会产生两倍于阈值流量的请求(突发流量)

临界限流失效
image

固定窗口限流器的代码如下:

public class CounterRateLimiter {
    private long windowSize;
    private int permitPerWindow;
    private int counter;
    private long latestWindowStartTime;

    public CounterRateLimiter(long _windowSize, int _permitPerWindow) {
        windowSize = _windowSize;
        permitPerWindow = _permitPerWindow;
        counter = 0;
        latestWindowStartTime = System.currentTimeMillis();
    }
	/**
	高并发下使用阻塞锁
	*/
    public synchronized boolean tryAcquire() {
        long currentTime = System.currentTimeMillis();
        if(currentTime - latestWindowStartTime < windowSize) {
            if(counter < permitPerWindow) {
                counter++;
                return true;
            }
            return false;
        }
        counter = 1;
        latestWindowStartTime = currentTime;
        return true;
    }
}

滑动窗口限流

滑动窗口限流算法是固定窗口限流算法的升级版,解决了固定窗口限流临界两倍阈值流量的问题,但是滑动窗口的实现和维护比较麻烦,并且限流不够平滑,无法适应突增流量,滑动窗口限流算法实现如下:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class SlideWindowRateLimiter implements RateLimiter{
    private final int windowSecond;
    private final int subWindowSecond;
    private final int subWindowCount;
    private final int permitPerWindow;
    private final int[] counter;
    private int currentSubWindowIndex;
    private long latestWindowStartTime;

    public SlideWindowRateLimiter(int _windowSecond, int _subWindowSecond, int _permitPerWindow) {
        windowSecond = _windowSecond;
        subWindowSecond = _subWindowSecond;
        subWindowCount = windowSecond / _subWindowSecond;
        permitPerWindow = _permitPerWindow;
        counter = new int[subWindowCount];
        latestWindowStartTime = System.currentTimeMillis();
        currentSubWindowIndex = 0;
    }

    public synchronized boolean tryAcquire() {
        long elapsedMillis = System.currentTimeMillis() - latestWindowStartTime;
        int elapsedTime = (int) Math.ceil(elapsedMillis / (subWindowSecond * 1000.0));
        if(elapsedTime > 0) {
            for(int i = 0; i < elapsedTime; i++) {
                currentSubWindowIndex = (currentSubWindowIndex + 1) % subWindowCount;
                counter[currentSubWindowIndex] = 0;
                latestWindowStartTime += subWindowSecond * 1000L;
            }
        }
        counter[currentSubWindowIndex]++;
        int currentRequest = Arrays.stream(counter).sum();
        return currentRequest <= permitPerWindow;
    }
}

漏桶算法

滑动窗口限流解决了固定窗口的流量超过限流阈值的问题,但是窗口限流固有的一个缺陷没有解决,即限流不够平滑,同时无法应对突增流量,水桶算法的思想是,请求按照任意速度进入系统排队,而系统以一个固定的速率处理这些请求。虽然实现了了流量的平滑限流,但是在高并发情况下,请求的处理速率配置不当容易导致请求积压。

漏桶算法
image

令牌桶算法

令牌桶算法是水桶算法的进一步改进,水桶算法无法应对高并发以及突增流量,那令牌桶怎么解决这个问题呢?

  • 按照固定速率向桶中投放令牌
  • 请求到来时,首先查看桶中是否有令牌,有的话,允许请求通过,并且令牌数-1,否则,触发限流策略
令牌桶算法
image
令牌桶能够做到流量的平滑限流,同时能够处理突增流量(令牌会持续增加)。代码如下:
public class TokenBucketRateLimiter {

    private int capacity;
    private int speed;
    private int currentTokens;
    private long latestSupplyTime;

    public TokenBucketRateLimiter(int _capacity, int _speed) {
        capacity = _capacity;
        speed = _speed;
        currentTokens = 0;
        latestSupplyTime = System.currentTimeMillis();
    }

    public synchronized boolean tryAcquire() {
        supply();
        if(currentTokens == 0) return false;
        latestSupplyTime = System.currentTimeMillis();
        currentTokens--;
        return true;
    }

    private void supply() {
        long current = System.currentTimeMillis();
        int suppliedTokens = (int)((current - latestSupplyTime) * speed / 1000.0);

        currentTokens = Math.min(currentTokens + suppliedTokens, capacity);

        latestSupplyTime = current;
    }
}

并不存在完美的限流算法,需要根据场景选择最合适的限流算法

限流算法 适用场景
固定窗口 对限流精度要求极低的场景,比如管理后台接口限流、日志上报限流等场景
滑动窗口 适合高精度限流(M秒内N次),但不允许有突增流量
漏桶 适合后端处理能力恒定的场景,比如音视频流控,要求严格匀速
令牌桶 适用于需要兼顾平滑性与突发能力,非常适合分布式系统下的高并发限流
posted @ 2025-12-14 20:02  xxs不是小学生  阅读(2)  评论(0)    收藏  举报