限流方案及使用场景分析

119 阅读8分钟

写作本文的目的在于项目内技术的总结,侧重分享自己的理解,代码部分参考ChatGPT。
限流的作用:流量控制、风控。

1、常见限流算法
1.1 固定(时间)窗口

概念:固定时间窗口(单位时间)内限制请求的数量;
优点:算法非常简单,易于实现和理解;
缺点:存在明显的临界问题;

临界问题:假设限流阀值为5个请求,单位时间窗口是1s,如果我们在单位时间内的前0.8-1s和1-1.2s,分别并发5个请求。虽然都没有超过阀值,但是如果算0.8-1.2s,则并发数高达10,已经超过单位时间1s不超过5阀值的定义。


public class FixedWindowRateLimiter {
    private final int limit; // 限制每秒的请求数
    private final long windowSizeInMillis; // 时间窗口大小(毫秒)
    private AtomicInteger counter = new AtomicInteger(0);
    private long windowStart = System.currentTimeMillis();

    public FixedWindowRateLimiter(int limit, long windowSizeInMillis) {
        this.limit = limit;
        this.windowSizeInMillis = windowSizeInMillis;
    }

    public boolean tryAcquire() {
        long now = System.currentTimeMillis();
        if (now - windowStart > windowSizeInMillis) {
            // 新的时间窗口
            counter.set(0);
            windowStart = now;
        }
        return counter.incrementAndGet() <= limit;
    }

    public static void main(String[] args) throws InterruptedException {
        FixedWindowRateLimiter limiter = new FixedWindowRateLimiter(5, 1000); // 每秒5个请求
        for (int i = 0; i < 10; i++) {
            System.out.println("Request " + (i + 1) + ": " + limiter.tryAcquire());
            Thread.sleep(200); // 模拟请求间隔
        }
    }
}

1.2 滑动窗口(可以解决固定窗口临界值的问题)

概念:将单位时间周期分为n个小周期,根据时间滑动删除过期的小周期.
优点:相比固定窗口,滑动窗口能更精确地限流。
缺点:1、瞬时高并发场景应对能力有限;
2、维护每个请求的精确时间戳,引起额外开销。

public class SlidingWindowRateLimiterWithSubWindows {
    private final LinkedList<Window> subWindows = new LinkedList<>(); // 存储小窗口
    private final int maxRequests; // 大窗口内的最大请求数
    private final long windowSizeInMillis; // 大窗口时间长度
    private final long subWindowSizeInMillis; // 小窗口时间长度
    private int totalRequestCount; // 当前大窗口内的总请求数

    public SlidingWindowRateLimiterWithSubWindows(int maxRequests, long windowSizeInMillis, int subWindowCount) {
        this.maxRequests = maxRequests;
        this.windowSizeInMillis = windowSizeInMillis;
        this.subWindowSizeInMillis = windowSizeInMillis / subWindowCount;
        this.totalRequestCount = 0;
    }

    public synchronized boolean isAllowed() {
        long currentTime = System.currentTimeMillis();
        // 清理过期的小窗口
        while (!subWindows.isEmpty() && currentTime - subWindows.peek().startTime >= windowSizeInMillis) {
            Window expiredWindow = subWindows.poll(); // 移除队列头部的过期小窗口
            totalRequestCount -= expiredWindow.requestCount; // 减少总请求数
        }

        // 判断是否允许当前请求
        if (totalRequestCount < maxRequests) {
            // 如果当前大窗口内的请求未达到上限,则记录当前请求
            addRequest(currentTime);
            return true;
        } else {
            // 否则拒绝请求
            return false;
        }
    }

    private void addRequest(long currentTime) {
        // 如果当前队列为空或当前请求不在队列尾部的时间范围内,则创建一个新的小窗口
        if (subWindows.isEmpty() || currentTime - subWindows.peekLast().startTime >= subWindowSizeInMillis) {
            subWindows.offer(new Window(currentTime)); // 添加新窗口
        }

        // 在当前小窗口中记录请求
        Window currentWindow = subWindows.peekLast();
        currentWindow.requestCount++;
        totalRequestCount++;
    }

    private static class Window {
        long startTime; // 小窗口的起始时间
        int requestCount; // 小窗口内的请求数量

        public Window(long startTime) {
            this.startTime = startTime;
            this.requestCount = 0;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SlidingWindowRateLimiterWithSubWindows rateLimiter = new SlidingWindowRateLimiterWithSubWindows(5, 1000, 5); // 1秒内最多允许5次请求,分为5个小窗口

        for (int i = 1; i <= 10; i++) {
            System.out.println("请求 " + i + ": " + (rateLimiter.isAllowed() ? "通过" : "被拒绝"));
            Thread.sleep(200); // 模拟请求间隔
        }
    }
}
1.3 漏桶限流

概念:漏桶算法模拟了一个固定容量的漏桶,桶中水以固定速率流出,水代表请求流量。如果桶满了,多余的水(请求)会被丢弃;
优点:1、平滑限制请求的处理速度;
2、调整桶的大小和漏出速率来满足不同的限流需求;
缺点:无法处理短期的突发流量;

public class LeakyBucketRateLimiter {
    private final int capacity;   // 漏桶容量
    private final int rate;       // 漏桶出水速率(每秒处理多少请求)
    private int water;            // 当前水量(请求数)
    private long lastLeakTime;    // 上次漏水时间

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

    public synchronized boolean tryAcquire() {
        long currentTime = System.currentTimeMillis();
        // 根据时间计算漏出的水量
        long elapsedTime = currentTime - lastLeakTime;
        int leakedWater = (int) (elapsedTime / 1000 * rate);
        if (leakedWater > 0) {
            water = Math.max(0, water - leakedWater);  // 更新水量
            lastLeakTime = currentTime;
        }
        if (water < capacity) {
            water++;  // 接收请求
            return true; // 请求成功
        } else {
            return false; // 请求被拒绝
        }
    }
}
1.4 令牌桶限流(可以处理突发流量)

概念:令牌桶算法允许请求以固定速率生成令牌。每个请求消耗一个令牌,如果桶内有足够的令牌,请求被允许处理。如果没有令牌,请求被拒绝或排队等待。允许一定的突发流量。
优点:能够处理突发流量
缺点:实现复杂,令牌维护开销大

区别漏桶法:令牌桶是生成速率固定,可以处理瞬时流量,因为之前生成的令牌可以堆积;漏桶法是流出速率固定,可以平滑输出;

public class TokenBucketRateLimiter {
    private final int capacity;    // 令牌桶容量
    private final int rate;        // 令牌生成速率(每秒生成多少令牌)
    private int tokens;            // 当前令牌数
    private long lastRefillTime;   // 上次令牌生成时间

    public TokenBucketRateLimiter(int capacity, int rate) {
        this.capacity = capacity;
        this.tokens = capacity;
        this.rate = rate;
        this.lastRefillTime = System.currentTimeMillis();
    }

    public synchronized boolean tryAcquire() {
        long currentTime = System.currentTimeMillis();
        // 计算生成的令牌数
        long newTokens = (currentTime - lastRefillTime) / 1000 * rate;
        if (newTokens > 0) {
            tokens = Math.min(capacity, tokens + (int) newTokens);
            lastRefillTime = currentTime;
        }
        if (tokens > 0) {
            tokens--;  // 消耗一个令牌
            return true;  // 请求成功
        } else {
            return false; // 请求被拒绝
        }
    }
} 
2、限流模式
2.1 单机限流

概念:限流逻辑只在单个服务实例内执行

2.2 分布式限流

概念:针对分布式系统或多实例部署的场景

3、常见限流实现
3.1 Sential(滑动窗口)

支持单机和分布式限流;其核心限流算法包括固定窗口限流、令牌桶算法、漏桶算法、并发线程数限制等。

3.2 RRatelimiter(分布式:令牌桶)

知识点:基于Redission的分布式限流
常用方法:
1、trySetRate(RRateLimiter.RateType rateType, long rate, long interval, RRateLimiter.IntervalUnit unit)

  • 设置限流速率。

  • 参数说明

    • rateType: 限流类型(OVERALLPER_CLIENT)。全局限流OVERALL)是指对整个系统的请求进行统一的限流,所有的请求共享同一个限流规则,不管这些请求来自于哪个客户端或来源。每客户端限流PER_CLIENT)是指根据每个客户端单独进行限流,每个客户端都有自己独立的限流器和令牌池。
    • rate: 每个时间单位生成的令牌数。
    • interval: 时间单位大小。
    • unit: 时间单位类型(秒、分钟、小时等)。
  • 示例

    java
    复制代码
    rateLimiter.trySetRate(RRateLimiter.RateType.OVERALL, 10, 1, RRateLimiter.IntervalUnit.SECONDS);
    

    每秒生成 10 个令牌。
    2、tryAcquire()

  • 尝试获取一个令牌,非阻塞。返回 true 表示成功获取令牌,false 表示限流。

  • 示例

    java
    复制代码
    if (rateLimiter.tryAcquire()) {
        // 获取到令牌,允许请求
    }
    

    3、acquire()

  • 阻塞式获取一个令牌。如果没有可用的令牌,会等待直到获取到令牌。

  • 示例

    java
    复制代码
    rateLimiter.acquire();  // 阻塞直到获取到令牌
    
import org.redisson.api.RRateLimiter;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

public class RedisRateLimiterExample {
    public static void main(String[] args) {
        // 配置 Redis 连接
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        RedissonClient redisson = Redisson.create(config);

        // 创建一个限流器,每秒生成 5 个令牌
        RRateLimiter rateLimiter = redisson.getRateLimiter("myRateLimiter");
        rateLimiter.trySetRate(RRateLimiter.RateType.OVERALL, 5, 1, RRateLimiter.IntervalUnit.SECONDS);

        for (int i = 0; i < 10; i++) {
            if (rateLimiter.tryAcquire()) {
                System.out.println("Request allowed: " + i);
            } else {
                System.out.println("Request blocked: " + i);
            }
        }

        redisson.shutdown();
    }
}

3.3 Guava RateLimiter(单机:令牌桶)

public class GuavaRateLimiterExample {
    public static void main(String[] args) {
        RateLimiter rateLimiter = RateLimiter.create(5.0);  // 每秒生成 5 个令牌

        for (int i = 0; i < 10; i++) {
            if (rateLimiter.tryAcquire()) {
                System.out.println("Request allowed: " + i);
            } else {
                System.out.println("Request blocked: " + i);
            }
        }
    }
}

4、不同场景选用
4.1 网关层面

1)(略)配置 Redis 和 Redisson 客户端
(2)创建一个自定义的 Global Filter,在网关层通过 RRateLimiter 进行全局限流。

import org.redisson.api.RRateLimiter;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class RateLimitGlobalFilter implements GlobalFilter, Ordered {

    private final RRateLimiter rateLimiter;

    @Autowired
    public RateLimitGlobalFilter(RedissonClient redissonClient) {
        // 初始化限流器
        this.rateLimiter = redissonClient.getRateLimiter("gateway-global-rate-limiter");
        // 配置限流规则:每秒生成 100 个令牌,表示每秒最多 100 个请求
        this.rateLimiter.trySetRate(RRateLimiter.RateType.OVERALL, 100, 1, RRateLimiter.IntervalUnit.SECONDS);
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, org.springframework.cloud.gateway.filter.GatewayFilterChain chain) {
        // 尝试获取令牌
        if (rateLimiter.tryAcquire()) {
            // 如果成功获取令牌,继续执行下一个过滤器
            return chain.filter(exchange);
        } else {
            // 如果限流生效,返回 HTTP 429 Too Many Requests
            exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
            return exchange.getResponse().setComplete();
        }
    }

    @Override
    public int getOrder() {
        // 设置全局过滤器的顺序,越小越优先执行
        return -1;
    }
}
4.2 针对不同接口QPS限流

场景:调用(API)接口时的限流
*方案:自定义注解 + AOP *
实现方式举例:
(1)定义自定义注解 @RateLimit

java
复制代码
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    // 每秒最多允许的请求数
    int qps() default 5;
    
    // 限流器的唯一标识(比如 API 名称)
    String key() default "";
}

(2)配置 Redisson 客户端

java
复制代码
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        return Redisson.create(config);
    }
}

(3)AOP 切面类,实现限流逻辑

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.redisson.api.RRateLimiter;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class RateLimitAspect {

    @Autowired
    private RedissonClient redissonClient;

    @Around("@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
        String key = rateLimit.key();
        int qps = rateLimit.qps();

        // 创建 RRateLimiter,基于注解中的 key 和 qps
        RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
        rateLimiter.trySetRate(RRateLimiter.RateType.OVERALL, qps, 1, RRateLimiter.IntervalUnit.SECONDS);

        // 检查是否有可用的令牌
        if (rateLimiter.tryAcquire()) {
            // 如果允许通过,继续执行方法
            return joinPoint.proceed();
        } else {
            // 如果被限流,抛出限流异常或者返回限流响应
            throw new RuntimeException("Rate limit exceeded. Please try again later.");
        }
    }
}

(4)应用自定义注解 @RateLimit 到 API 接口

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

    // 对该接口限流,每秒最多允许 10 个请求
    @RateLimit(key = "test-api", qps = 10)
    @GetMapping("/api/test")
    public String testApi() {
        return "API Request allowed.";
    }

    // 另一个限流接口,每秒最多允许 3 个请求
    @RateLimit(key = "another-api", qps = 3)
    @GetMapping("/api/another")
    public String anotherApi() {
        return "Another API Request allowed.";
    }
}
4.2.2 针对不同客户端限流

结合 调用方的标识(如 API Key用户IDIP 地址等)来创建和管理每个调用方的独立限流器。这样每个调用方都会有独立的令牌桶,并且它们的限流规则也可以不同。 示例根据 API Key 进行限流逻辑

import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

    private final RedissonClient redissonClient;

    @Autowired
    public RateLimitedApiController(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }

    @GetMapping("/resource")
    public String accessResource(@RequestHeader("api-key") String apiKey) {
        // 1. 根据 API Key 动态创建和获取限流器
        RRateLimiter rateLimiter = redissonClient.getRateLimiter("rate-limiter-" + apiKey);

        // 2. 设置不同的限流规则,每个 API Key 的限流规则可以不同
        if ("API_KEY_1".equals(apiKey)) {
            // 为 API_KEY_1 设置每秒最多 10 个请求
            rateLimiter.trySetRate(RRateLimiter.RateType.OVERALL, 10, 1, RRateLimiter.IntervalUnit.SECONDS);
        } else if ("API_KEY_2".equals(apiKey)) {
            // 为 API_KEY_2 设置每秒最多 5 个请求
            rateLimiter.trySetRate(RRateLimiter.RateType.OVERALL, 5, 1, RRateLimiter.IntervalUnit.SECONDS);
        } else {
            // 默认限流规则,假设每秒最多 3 个请求
            rateLimiter.trySetRate(RRateLimiter.RateType.OVERALL, 3, 1, RRateLimiter.IntervalUnit.SECONDS);
        }

        // 3. 检查限流状态
        if (rateLimiter.tryAcquire()) {
            // 如果获取到令牌,允许访问资源
            return "Request allowed for API Key: " + apiKey;
        } else {
            // 如果限流,返回提示
            return "Too many requests for API Key: " + apiKey;
        }
    }
}