如何优雅地应对流量洪峰?令牌桶算法给你答案

哈喽,哈喽,大家好~ 我是你们的老朋友:保护小周ღ  


本期主要讲述,  令牌桶算法的核心原理,  配合 HandlerInterceptor 拦截器使用, 设计拒绝策略,  以及根据 IP 实现粒度更细的限流器,  清除长期不使用了限流器......  一起来看看叭~


本期收录于博主的专栏JavaEE_保护小周ღ的博客-优快云博客

适用于编程初学者,感兴趣的朋友们可以订阅,查看其它 “JavaEE基础知识”。

更多精彩敬请期待:保护小周ღ *★,°*:.☆( ̄▽ ̄)/$:*.°★* ‘ 


一、令牌桶算法简介

令牌桶算法(Token Bucket Algorithm) 是一种常用的网络流量整形(Traffic Shaping)速率限制(Rate Limiting)限流算法,它能够有效地控制网络流量或系统请求的速率,避免系统被突发流量压垮,保障系统的稳定性和可用性。

1.1 核心思想

想象一个桶,里面装着令牌(Token  想象成古代衙门判案时摔的令牌)。这个桶有一个固定的容量(Capacity 能装多少令牌),并且可以以恒定(也可以动态调整速率)的速率(Rate)向桶中添加令牌 (比如 1 秒中生成一个)。每个请求到达时(每一次判案),都需要从桶中获取一个令牌才能被处理。如果桶中没有足够的令牌,则请求会被拒绝或等待。

 这只是令牌桶算法的核心思想, 可以在此思想之上, 根据不同的需求, 场景, 进行调整.

1.2 算法特点

根据核心思想, 就比较容易的理解这个算法的使用场景, 作用, 特点.

主要作用: 

  • 流量整形: 将突发的流量整形为平滑的流量,避免网络拥塞.

  • 速率限制: 限制系统处理的请求速率,防止系统过载,  当桶中令牌消耗殆尽, 后续的请求, 就只能依赖于令牌的生成速率了,  比如:  令牌一秒生成一个, 此时请求就只能一秒处理一个.

  • 可以应对短暂的突发流量:  取决于桶的初始容量,  如果桶设置为满令牌, 短时间内是可以应对桶容量的请求, 如果超出了容量,  这个时候就需要做一些策略应对, 这个下文讲.

  • 精准控制: 可以为不同的请求分配不同的令牌获取速率(限流器), 后面举例子.

应用场景:

  • API 限流: 限制 API 的调用频率,防止 API 被滥用.

  • 系统资源保护: 限制系统资源的访问速率,防止系统过载.

  • 网络流量控制: 控制网络流量,避免网络拥塞。

优点:

  • 简单易实现: 算法原理简单,易于理解和实现.

  • 平滑流量: 能够有效地将突发流量整形为平滑的流量,避免系统被瞬间的流量洪峰冲垮.

  • 灵活性高: 可以通过调整桶的容量和令牌添加速率来控制流量速率.

缺点:

  • 无法应对大量的突发流量: 如果突发流量超过了桶的容量,则会导致部分请求被拒绝或等待, 这个时候需要做出一定的策略来应对.

  • 存在延迟: 请求需要等待令牌,在这个过程中可能会造成一定的延迟, 这个是在所难免的.


二、令牌桶算法在服务端的应用

2.1 基本功能实现

讲解配置: IDEA, JDK 8, SpringBoot 

最基本的核心功能, 代码如下:  后面会在此的基础之上, 逐步增加功能.

五个核心参数 :

  1.  tokens  当前桶中的令牌数量
  2.  capacity 桶的容量
  3.  TimeUnit 时间单位 , 这样设计就比较灵活, 
  4.  tokensPerUnit 每单位时间内生成的令牌数

需求: 15 分钟生成一个怎么写, 这还不简单,  TimeUnit.HOURS (小时 )

 tokensPerUnit  =  4, 一小时生成 4 个, 跟 15 分钟生成一个是一回事儿吧......

注意: 只有在生成令牌成功之后, 才会跟新上一次生成令牌的时间 lastRefillTime ,  因此不需要担心,  15 分钟的以前, 调用消耗令牌, 随后由其调用补充令牌,导致时间重置的问题. 

两个核心 API : 

  1. synchronized boolean tryAcquire()  消耗令牌, 注意线程安全问题
  2. refillTokens();  补充令牌

消耗令牌的时候会调用 refillTokens();  调用时机处于同步代码块内, 因此不需要添加 synchronized
synchronized 修饰一个实例方法时,它使用的对象锁是当前实例对象(即 this

import java.util.concurrent.TimeUnit;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * Author: 保护小周
 * Date: 2025-03-15
 * Time: 21:04
 */
public class TokenBucket {

    /**
     * 当前桶中的令牌数量
     */
    private int tokens;

    /**
     * 桶的容量
     */
    private final int capacity;

    /**
     * 时间单位
     */
    private final TimeUnit timeUnit;

    /**
     * 每单位时间生成的令牌数
     */
    private final long tokensPerUnit;

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

    /**
     * 令牌桶, 初始化
     * @param capacity  桶的最大容量
     * @param timeUnit  时间单位  支持 TimeUnit 的所有时间单位(如 SECONDS、MINUTES、HOURS 等),使得令牌生成速率的配置更加灵活。
     * @param tokensPerUnit  单位时间生成的令牌数
     */
    public TokenBucket(int capacity, TimeUnit timeUnit, long tokensPerUnit) {
        this.capacity = capacity;
        this.timeUnit = timeUnit;
        this.tokensPerUnit = tokensPerUnit;
    }

    // 核心 API
    /**
     * 尝试获取令牌
     * @return true:成功获取令牌,允许请求通过;false:没有足够的令牌,拒绝请求
     */
    public synchronized boolean tryAcquire() {
        refillTokens(); // 补充令牌
        if (tokens > 0) {
            tokens--; // 消耗一个令牌
            return true; // 允许请求通过
        }
        return false; // 没有足够的令牌,拒绝请求
    }

    /**
     * 补充令牌
     */
    private void refillTokens() {
        long now = System.currentTimeMillis();

        // 计算距离上次补充令牌的时间间隔
        long elapsedTime = now - lastRefillTime;
        // 将时间差转换为时间单位
        // TimeUnit.MILLISECONDS 的作用是将时间间隔从毫秒转换为指定的时间单位(例如秒、分钟等),以便正确计算令牌的补充数量。
        // timeUnit , 是需要我们指定的时间单位, 秒, 分钟,小时....
        long elapsedUnits = timeUnit.convert(elapsedTime, TimeUnit.MILLISECONDS);

        if (elapsedUnits > 0) {
            // 计算这段时间内应该生成的令牌数量,   elapsedUnits, 就描述了几个单位时间
            int newTokens = (int) (elapsedUnits * this.tokensPerUnit);
            // 最多就一次性将桶装满了.
            this.tokens = Math.min(capacity, this.tokens + newTokens);
            // 保存本次补充令牌的时间戳, 用于下次补充令牌计算时间间隔
            this.lastRefillTime = now;
        }
    }
}

2.2 基本功能测试

2.2.1 测试用例 1:基本功能测试

场景:

  • 令牌桶容量为 10。

  • 令牌生成速率为每秒 1 个令牌。

  • 模拟连续请求,验证令牌桶的限流效果。

public static void main(String[] args) throws InterruptedException {
        // 创建一个容量为 10,每秒生成 1 个令牌的令牌桶
        TokenBucket tokenBucket = new TokenBucket(10, TimeUnit.SECONDS, 1);

        // 模拟连续请求
        for (int i = 1; i <= 21; i++) {
            boolean result = tokenBucket.tryAcquire();
            System.out.println("请求 " + i + ": " + (result ? "通过" : "被限流"));
            Thread.sleep(300); // 模拟请求间隔
        }
}

考虑到程序执行, 是需要时间的, 然后跟 CPU 的执行速率, 调度等方面, 些许误差是允许的. 诸位也可以使用自己的电脑运行一下程序, 调整一下参数, 观察是否跟博主的结果是一样的呢, 


 2.2.2 测试用例 2:令牌补充测试

场景:

  • 令牌桶容量为 5。

  • 令牌生成速率为每秒 2 个令牌。

  • 模拟短时间内大量请求,验证令牌的补充机制。

public static void main(String[] args) throws InterruptedException {
        // 创建一个容量为 5,每秒生成 2 个令牌的令牌桶
        TokenBucket tokenBucket = new TokenBucket(5, TimeUnit.SECONDS, 2);

        // 模拟短时间内大量请求
        for (int i = 1; i <= 10; i++) {
            boolean result = tokenBucket.tryAcquire();
            System.out.println("请求 " + i + ": " + (result ? "通过" : "被限流"));
            if (i == 5) {
                System.out.println("等待 2 秒,让令牌桶补充令牌...");
                Thread.sleep(2000); // 等待 2 秒,让令牌桶补充令牌
            }
        }
    }


 2.2.3 测试用例 3:高并发测试

场景:

  • 令牌桶容量为 20。

  • 令牌生成速率为每秒 5 个令牌。

  • 模拟高并发场景,验证令牌桶的线程安全性。

// 记录执行次数
    public static volatile int count = 0;

    public synchronized static void addCount() {
        count++;
    }

    public static void main(String[] args) {

        // 创建一个容量为 20,每秒生成 5 个令牌的令牌桶
        TokenBucket tokenBucket = new TokenBucket(20, TimeUnit.SECONDS, 5);

        // 创建一个线程池,模拟高并发请求
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        // 提交 50 个任务
        for (int i = 1; i <= 50; i++) {
            int requestId = i;
            executorService.submit(() -> {
                boolean result = tokenBucket.tryAcquire();
                System.out.println("请求 " + requestId + ": " + (result ? "通过" : "被限流"));
                // 计数
                addCount();
            });
        }

        // main 主线程等待线程池将所有任务都执行完毕, 需要耐心等待 1 分钟
        try {
            // 等待所有任务执行完毕,最多等待 1 分钟
            if (!executorService.awaitTermination(1, TimeUnit.MINUTES)) {
                System.out.println("线程池未能在指定时间内完成任务");
            }
        } catch (InterruptedException e) {
            System.out.println("主线程在等待时被中断");
        }
        // 关闭线程池
        executorService.shutdown();

        System.out.println("50 是个任务, 总计处理任务: " + count);
    }

根据统计 确实提交了 50 个任务, 只不过, 根据令牌桶的速率限制, 有的能被放行, 有的会被限制. 总的来讲, 已经达到了要求, 测试通过!  诸位也可以使用代码自己跑一跑感受一下.


2.3 拒绝策略

这个标题主要描述了, 当请求来临时, 令牌桶中的令牌不够了, 应该怎么做.

2.3.1 直接拒绝-配合  HandlerInterceptor 的接口

描述:

  • 当令牌不足时,直接拒绝新到达的请求,并返回错误或提示信息。

  • 这是最简单的拒绝策略,适用于对实时性要求较高的场景。

优点:

  • 实现简单,性能高。

  • 可以快速响应请求,避免请求积压。

缺点:

  • 用户体验较差,请求被直接拒绝。

  • 不适合需要保证请求不丢失的场景。

适用场景:

  • 实时性要求高的场景,如 API 限流。

  • 请求可以重试的场景。

带入到 SpringBoot 项目中, 令牌桶算法, 配合 HandlerInterceptor 的接口实现类(请求拦截器). 实现针对所有请求的限流.当请求来临时, 被拦截器获取, 只有从令牌桶中获取了令牌才允许请求放行, 路由到指定的 API 执行.

先来了解一下什么是  HandlerInterceptor 接口

HandlerInterceptor  是 Spring MVC 框架中的一个接口,用于拦截请求并在处理请求的不同阶段执行自定义逻辑。它通常用于实现以下功能:(在 Web 层面 AOP 切面编程的一种)

  • 权限验证

  • 日志记录

  • 请求参数预处理, 请求到达路由之前做些什么....

  • 响应结果后处理, 请求到达路由, API 执行完成之后做些什么......

HandlerInterceptor 接口定义了三个方法,其中最常用的是 preHandle 方法。

下面我们将详细讲解 HandlerInterceptor 接口及其 preHandle 方法。

boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;

参数说明

  • HttpServletRequest request

    • 当前 HTTP 请求对象,可以从中获取请求参数、请求头等信息。

  • HttpServletResponse response

    • 当前 HTTP 响应对象,可以用于设置响应状态码、响应头等信息。

  • Object handler

    • 当前请求的处理器(通常是 HandlerMethod 对象),可以从中获取控制器和方法信息。

返回值

  • true

    • 表示继续执行后续的请求逻辑。

  • false

    • 表示中断请求处理流程,后续的拦截器和处理器不会执行。

常见用途

  1. 权限验证:

    • 检查用户是否登录或是否有权限访问当前资源。

    • 如果未通过验证,可以重定向到登录页面或返回错误响应。

  2. 日志记录:

    • 记录请求的 URL、参数、IP 地址等信息。

  3. 请求参数预处理:

    • 对请求参数进行校验或转换。

  4. 限流:

    • 使用令牌桶算法或其他限流算法控制请求的速率。

可以理解为门卫, 符合条件就放你进去, 否则就进行拦截, 不允许进入......

​以下是, HandlerInterceptor 的接口的 preHandle() 方法配合令牌桶限流的实现.

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 * Created with IntelliJ IDEA.
 * Description: 登录拦截器
 * Author: 保护小周
 * Date: 2023-08-07
 * Time: 17:39
 */
@Component
@Order(2)
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

    /**
     * 桶的容量
     */
    private int capacity = 10;

    /**
     * 每单位时间生成的令牌数
     */
    private long tokensPerUnit = 1l;

   
    
    /**
     * 预处理(请求的前置处理)回调⽅法<br/>
     * 返回值: <br/>true 流程继续;<br/>
     * false流程中断, 不会再调⽤其他的拦截器
     * @param request
     * @param response
     * @param handler
     * @return true 继续 false 中断
     * @throws Exception
     */
    @Override // 请求再到达服务器之前的前置处理
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        
        // 1. 获取或创建限流器, 
        TokenBucket rateLimiter = new TokenBucket(capacity, TimeUnit.SECONDS , tokensPerUnit);

        // 2. 当请求来临时尝试获取令牌, return true / false
        if (!rateLimiter.tryAcquire()) {
            // 如果限流器拒绝请求,直接向客户端返回错误响应
            sendErrorResponse(response, 429, "请求过于频繁,请稍后重试");
            // 拒绝路由请求
            return false;
        }

        // 请求放行, 路由至指定 API
        return true;
    }

    /**
     * 发送错误响应(JSON 格式)
     */
    private void sendErrorResponse(HttpServletResponse response, int code, String message) throws IOException {
        response.setStatus(code);
        response.setContentType("application/json;charset=UTF-8");

        // 构建 JSON 响应体
        Map<String, Object> errorResponse = new HashMap<>();
        errorResponse.put("code", code);
        errorResponse.put("message", message);
        errorResponse.put("data", null);

        // 将 JSON 写入响应
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.writeValue(response.getWriter(), errorResponse);
    }
}

如果客户端的请求频率, 超出了令牌的生成速率, 那么就会有以下效果,  响应中带有我们设置的信息, 提醒用户请求过于频繁, 请稍后重试, 也就意味着,  限制了系统处理的请求速率,防止系统过载.速率完全取决于令牌的生产速度.

思考: 

HandlerInterceptor 的接口的 preHandle() 方法配合令牌桶限流器,  是针对所有的客户端请求吗?  如果是, 限流器的参数应该如何设置?  有没有粒度更细的限流方式 ? 比如针对某一个 API, 针对某一个用户, 针对某一个请求.......


2.3.2  排队等待(Queueing)

排队等待是一种限流拒绝策略,当请求到达时,如果令牌不足,则将请求放入队列中等待,直到有可用的令牌。这种方式可以保证请求不丢失,适合需要高可靠性的场景。

适用场景

  • 需要保证请求不丢失的场景:

    • 用户登录:用户的登录请求不能丢失,必须确保每一个用户登录请求都能够处理。

    • 消息队列:消息需要被可靠地传递和处理。

  • 可以接受一定延迟的场景:

    • 异步任务处理:任务可以稍后执行,不需要立即响应。

    • 批量数据处理:数据可以分批处理,不需要实时响应。

代入到实际的应用中,  基本的令牌桶限流器,  两个核心方法, 获取令牌 / 生产令牌,  根据返回值的 true / false 来决定是否逻辑是否放行.  那么如何确保用户的请求不丢失呢? 这就涉及到两个核心的东西,  队列 ,  Runnable 接口.

队列好说, 大家应该都比较了解,  主要是了解一下 Runnable 接口在这个过程中的应用.

Runnable 包含一段可执行的逻辑, 限流执行

Runnable 是 Java 中一个非常重要的接口,用于表示一个可以执行的任务。它在多线程编程、异步任务处理、事件驱动编程等场景中广泛应用。
创建线程的其中一种方式, 就是实现  Runnable 接口.

  • run() 方法:

    • 定义了任务的具体逻辑。

    • 任何实现了 Runnable 接口的类都必须实现 run() 方法。

  • 函数式接口:

    • Runnable 是一个函数式接口,可以使用 Lambda 表达式或方法引用来创建实例。

如果想要自定义的函数式接口, 需要在实现类上标注 @FunctionalInterface 注解

Runnable 最常见的应用场景是多线程编程。通过将任务封装在 Runnable 中,可以将其传递给 Thread 类,启动一个新线程来执行任务。

public class RunnableExample {
    public static void main(String[] args) {
        // 创建一个 Runnable 任务, 这是采用 Lamdba 表达式创建
        Runnable task = () -> {
            System.out.println("任务正在执行,线程: " + Thread.currentThread().getName());
        };

        // 创建一个线程并启动
        Thread thread = new Thread(task);
        thread.start();
    }
}

由此, 我们就可以在限流器代码中,使用 Runnable 用于封装需要限流的任务。当令牌可用时,直接执行任务;当令牌不足时,将任务放入队列中等待令牌生产。

在此基础之上, 就需要对令牌桶限流类的基本功能进行修改: 

在使用限流器的情况下, 当令牌不足时, 希望不要拒绝请求的逻辑, 而是等待有令牌时再执行.

设计如下: 

1. 添加线程安全的阻塞队列.

2. 添加方法, 使用 Runnable request 作为参数

  • 当令牌可用时 ,  直接处理请求
  • 如果在令牌可用且有多余的令牌这个基础之上,  队列中有任务, 唤醒, 队列处理任务(消费线程维护)
  • 令牌不足时, 将任务, 放入队列当中,  通知消费者线程有新的任务, 去消费任务.
  • 当队列为空时, 进入阻塞等待状态,  当队列提交了新的任务时, 唤醒,  当令牌可用, 且有任务时, 也会唤醒.
  • 当队列任务满了的时候的拒绝策略.
  • 注意线程安全问题

聪明的小伙伴根据需求, 自己在基本的令牌桶算法的基础之上, 进行调整,  有自己的想法也可以加入其中...

 做出如下调整:  这是在基础版本上添加代码.

 /**
     * 请求队列,用于存储被限流的请求
     */
    private final LinkedBlockingQueue<Runnable> requestQueue;

    /**
     * 令牌桶, 初始化
     * @param capacity  桶的最大容量
     * @param timeUnit  时间单位  支持 TimeUnit 的所有时间单位(如 SECONDS、MINUTES、HOURS 等),使得令牌生成速率的配置更加灵活。
     * @param tokensPerUnit  单位时间生成的令牌数
     * @param queueSize  请求对了的长度...
     */
    public TokenBucket(int capacity, TimeUnit timeUnit, long tokensPerUnit, int queueSize) {
        this.capacity = capacity;
        this.timeUnit = timeUnit;
        this.tokensPerUnit = tokensPerUnit;
        this.requestQueue = new LinkedBlockingQueue<>(queueSize);

        // 启动一个后台线程处理队列中的请求
        startQueueConsumer();
    }


/**
     * 处理任务
     * @param request 需要处理的任务
     */
    public void handleRequest(Runnable request) {
        if (tryAcquire()) {
            // 令牌可用,直接处理请求
            request.run();
            
            if (tokens > 0 && !requestQueue.isEmpty()) { // 还有多的令牌,且队列不为空
                requestQueue.notify(); // 唤醒
            }
        } else {
            // 令牌不足,将请求放入队列
            synchronized (requestQueue) {
                if (requestQueue.offer(request)) {
                    System.out.println("Request added to queue");
                    requestQueue.notify(); // 通知消费者线程有新的请求
                } else {
                    // 队列已满,直接拒绝
                    System.out.println("Request rejected: Queue is full");
                }
            }
        }
    }


    /**
     * 启动队列消费者线程,处理队列中的请求
     */
    private void startQueueConsumer() {
        Thread consumerThread = new Thread(() -> {
            while (true) {
                synchronized (requestQueue) {
                    while (requestQueue.isEmpty()) {
                        try {
                            requestQueue.wait(); // 等待队列中有请求
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                    // 从队列中取出请求
                    Runnable request = requestQueue.poll();
                    if (request != null) {
                        // 尝试获取令牌
                        while (!tryAcquire()) {
                            try {
                                Thread.sleep(100); // 等待一段时间后重试, 可以根据令牌生产的速率进行调整
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        // 令牌可用,处理请求
                        request.run();
                    }
                }
            }
        });
        consumerThread.setDaemon(true); // 设置为守护线程
        consumerThread.start();
    }
测试用例说明

1. 令牌桶配置

  • 容量(capacity): 10

  • 时间单位(timeUnit): 秒(TimeUnit.SECONDS

  • 令牌生成速率(tokensPerUnit): 每秒 2 个令牌

  • 队列大小(queueSize): 20  确保能够接受的了, 全部请求,  咱们还没写拒绝策略

2. 模拟请求

  • 模拟 30 个请求,每个请求之间间隔 50 毫秒。

  • 每个请求的处理逻辑是打印请求 ID 和线程名称,并模拟 200 毫秒的处理时间。

3. 预期行为

  1. 前 10 个请求:

    • 令牌桶初始有 10 个令牌,前 10 个请求可以直接获取令牌并立即执行。

  2. 第 11 到 30 个请求:

    • 前 10 个令牌因为有桶容量, 所以能够处理, 在此期间,  每条请求, 需要 200 + 50 毫秒处理,  这个过程中, 至少可以生产:  10 * 250 * 2 = 4 个令牌

    • 在执行的过中在执行这四个令牌,  至少可以生产 : 4 * 250 * 2 = 2 个令牌
    • 也就是说至少前 16 个请求会直接通过, 不会进入队列,  注意: 是至少, 因为也不能因此忽略程序执行所需的时间.  理想状态下, 执行16 个请求, 需要 3. 5 秒(主动的延时), 加上执行时本身需要的时间,  应该是 17 ~ 18 个请求是不需要进入队列的.
    • 令牌不足时,后续的请求会被放入队列中等待。

    • 每秒生成 2 个令牌,消费者线程会从队列中取出请求并处理。

  3. 队列满时:

    • 如果队列已满(20 个请求),新的请求会被直接拒绝。

public static void main(String[] args) throws InterruptedException {
        // 创建一个令牌桶:容量为 10,每秒生成 2 个令牌,队列大小为 20
        TokenBucket tokenBucket = new TokenBucket(10, TimeUnit.SECONDS, 2, 20);

        // 模拟 30 个请求
        for (int i = 1; i <= 30; i++) {
            int requestId = i;
            // 提交任务
            Runnable request = () -> {
                System.out.println("处理请求: " + requestId + ",线程: " + Thread.currentThread().getName());
                try {
                    // 模拟请求处理时间
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            };

            // 提交请求
            tokenBucket.handleRequest(request);

            // 模拟请求到达的时间间隔
            Thread.sleep(50);
        }

        // 主线程等待一段时间,确保所有请求都被处理
        Thread.sleep(5000);
        System.out.println("测试结束");
    }

大家也可以尝试着不同的参数, 来进行测试, 感受一下效果.

思考: 

当令牌桶中令牌被消耗殆尽, 任务队列也已经装满, 此时, 再次提交任务,  队列此时的拒绝策略该怎么设计呢? 

1. 丢弃任务, 直接响应请求过于频繁?

2. 动态扩容队列 ?

那么如果换作是你, 要如何设计呢?


思考:  

能否实现动态调整令牌生产速率 tokensPerUnit ,根据系统负载动态调整令牌生成速率。当系统负载较高时,降低令牌生成速率;当系统负载较低时,提高令牌生成速率。


2.4 根据 IP 限流,粒度更细的限流器

根据  2.3.1 直接拒绝-配合  HandlerInterceptor 的接口 中使用的场景,  我提到的思考题,  此时  preHandle 方法中实现的限流器, 是针对于所有的客户端请求吗?   答案: 是的

再思考一下, 如果用户特别多的话, 那么势必就需要给限流器, 提供速率更快的参数,  那么限流器这个体量无疑是庞大的, 及其影响服务器的性能

 解决方案:  如果我们针对每一个客户端提供一个限流器,  当前限流器只服务于每一个用户, 是不是就可以解决单个限流器体量大, 以及更加灵活的根据每个用户的 "态度" 进行限制呢? 

设计思想: 

1. 从请求中, 获取每个请求背后的用户 ip ,  作为,  key 值,  并实例一个限流器作为 value 

2. 当该请求用户 ip 已经分配限流器时,   根据 请求 ip 获取对应的限流器, 并使用限流器当中的获取令牌方法, 判断令牌是否可用.

注意: 本次功能添加只涉及到限流器的使用, 并不需要修改限流器的解构.

LoginInterceptor 类: 属性, 方法添加 --- 2.3.1 中代码基础之上 修改

// 存储每个 IP 地址的限流器, 注意使用线程安全的 map
    private final ConcurrentHashMap<String, TokenBucket> ipRateLimiterMap = new ConcurrentHashMap<>();

获取客户端 IP 地址, 这个方法, 直接问 ai 的, 毕竟咱是真的不知道咋写啊. 

/**
     * 获取客户端 IP 地址
     * @param request HttpServletRequest
     * @return 客户端 IP 地址
     */
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }

 preHandle () 方法配合 ip 限流器的使用~

/**
     * 预处理(请求的前置处理)回调⽅法<br/>
     * 返回值: <br/>true 流程继续;<br/>
     * false流程中断, 不会再调⽤其他的拦截器
     * @param request
     * @param response
     * @param handler
     * @return true 继续 false 中断
     * @throws Exception
     */
    @Override // 请求再到达服务器之前的前置处理
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 1. 获取客户端 IP 地址
        String clientIp = getClientIp(request);
        log.info(clientIp);

        // 获取或创建限流器,  ip 为 key , 限流器为 value
        // 工作原理 :
        // 如果 Map 中已经存在该 key,则返回对应的值,而不调用 mappingFunction。
        // 如果 Map 中不存在该 key,则调用 mappingFunction 来计算该 key 的值,然后将 key 和计算出来的值一起插入 Map 中。
        TokenBucket rateLimiter = ipRateLimiterMap.computeIfAbsent(clientIp, k ->
                new TokenBucket(capacity, TimeUnit.SECONDS, tokensPerUnit ,queueSize));

        // 2. 当请求来临时尝试获取令牌, return true / false
        if (!rateLimiter.tryAcquire()) {
            // 如果限流器拒绝请求,直接向客户端返回错误响应
            sendErrorResponse(response, 429, "请求过于频繁,请稍后重试");
            // 拒绝路由请求
            return false;
        }

        // 请求放行, 路由至指定 API
        return true;
    }

怎么测试呢, 这个就需要大家准备两个客户端 (web前端) 试试咯,  大量的发请求尝试一下,   这里有个问题, 就是多个客户端公用一个外网 ip 的情况,  那可以使用 ip + 设备信息 作为 key 嘛.

或者写一个模拟代码测试一下,  可以用多个线程来模拟客户端, 线程名视作 ip 为 key , 针对 每个线程设置一个限流器,  然后判断需求是否满足即可. 

思考: 

到这里大家觉得这个功能是否完善了呢,   有没有 bug? 
答案是: 有的, 有的, 这样的 bug 有两个

1. 咱们使用一个线程安全的 map, 用于维护每一个 ip 用户的 限流器, 如果服务器到启动后有数万 ip 访问过, 那么 map 会挂载数万的限流器, 体量极大,  但是同一时间内, 只有几百,几千的用户在线, 大量的用户并没有使用了,  聪明的各位, 想到了问题的关键点了没,  完全可以清理掉, 一定时间内不再使用服务器的一些用户的 限流器, 以提供 map 的性能.

2. 在分布式环境下,  当前服务器只能记录, 本机获取的请求中的 ip , 限流器,  在其他服务器当中,  同一 ip 请求的用户也有可能, 会分配执行, 也就是说, 同一 ip 在分布式环境下, 可能有多个限流器, 这就没有达到限流的作用. 不知道请求会分配给那个服务器执行. 咱们需要一个 ip 在多个服务器中出现的情况下, 共享一个为其分配的限流器. 怎么办呢, 好办,  把 map , 换成 redis 存储即可,  因为  redis 是可以单独部署的,  多个服务器都可以根据 ip key, 获取与之对应的限流器.  

思考: 
如果察觉到, 某个 ip 的请求过于频繁,  不停的请求, 应该怎么办,  这个时候可以, 根据请求频率等,可以视作为 恶意攻击, 蓄意冲击 服务器.

方案: 动态的为其分配速率低的限流器, 或者直接拒绝请求.....


2.5 清除超时的限流器

当前标题的目的, 就是为了解决 2.4 中提到的,  定时清理一些,  一定时间内没有使用的限流器,  以提升系统的性能. 

设计思想: 

程序启动,  启动 ScheduledExecutorService 线程池 , 启动定时任务,定期清理过期的限流器(比如每分钟扫描一次) 
如果限流器超过过期时间未被使用,则移除 :  当前系统时间 -  限流器最后使用时间 > 设置的过期时间(比如 5 分钟过期 5 * 60 * 1000 ) 

限流器类, 属性增加, 需要记录, 最后访问时间 (单位: 毫秒), 过期时间 (单位: 毫秒)

 TokenBucket 限流器类的属性增加, 以及初始化操作, 其余方法属性不进行修改

/**
     * 最后访问时间 (单位: 毫秒)
     */
    private long lastAccessTime;

    /**
     * 过期时间(单位:毫秒)
     */
    private long expireTime;

    /**
     * 初始化构造方法
     * @param capacity       桶的容量
     * @param tokensPerUnit  每单位时间生成的令牌数
     * @param timeUnit       时间单位, 支持 TimeUnit 的所有时间单位(如 SECONDS、MINUTES、HOURS 等),使得令牌生成速率的配置更加灵活。
     * @param queueSize      队列的长度
     */
    public TokenBucket(int capacity, TimeUnit timeUnit, long tokensPerUnit, int queueSize) {
        this.capacity = capacity;
        this.tokensPerUnit = tokensPerUnit;
        this.timeUnit = timeUnit;
        this.lastRefillTime = System.currentTimeMillis();
        this.tokens = capacity;
        this.requestQueue = new LinkedBlockingQueue<>(queueSize);
        this.lastAccessTime = System.currentTimeMillis(); // 最后访问时间
        this.expireTime = 5 * 60 * 1000; // 默认过期时间:5 分钟

        // 启动一个后台线程处理队列中的请求
        startQueueConsumer();
    }


    public long getExpireTime() {
        return expireTime;
    }

    public void setExpireTime(long expireTime) {
        this.expireTime = expireTime;
    }

    public long getLastAccessTime() {
        return lastAccessTime;
    }

    public void setLastAccessTime(long lastAccessTime) {
        this.lastAccessTime = lastAccessTime;
    }

了解一下 ScheduledExecutorService 

ScheduledExecutorService 简介

ScheduledExecutorService 是 Java 中 ExecutorService 的一个子接口,专门用于调度任务。它可以安排任务在指定的延迟后执行,或者以固定的时间间隔周期性执行。它是 java.util.concurrent 包的一部分,常用于多线程应用中管理定时任务。

核心功能

  1. 延迟执行:安排任务在指定延迟后执行一次。

  2. 周期性执行:安排任务以固定的时间间隔或固定的延迟重复执行。

  3. 线程管理:通过线程池管理任务的执行。


常用方法

  1. schedule(Runnable command, long delay, TimeUnit unit):

    • 安排一个任务在指定延迟后执行一次。

    • 示例:5 秒后执行任务。

  2. schedule(Callable<V> callable, long delay, TimeUnit unit):

    • 安排一个带返回值的任务(Callable)在指定延迟后执行。

  3. scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):   我们使用这个

    • 安排任务以固定的频率重复执行。

    • 第一次执行在 initialDelay 后开始,之后每隔 period 时间执行一次。

    • 如果某次执行时间超过 period,下一次执行会立即开始。

  4. scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):

    • 安排任务以固定的延迟重复执行。

    • 第一次执行在 initialDelay 后开始,之后每次执行完成后,等待 delay 时间再开始下一次执行。

  5. 线程池ScheduledExecutorService 使用线程池来执行任务。创建时可以指定线程池的大小。

 使用类 :  ScheduledExecutorService 生命周期, 伴随着系统启动而启动, 随着系统关闭而关闭 

// 定时任务线程池,用于清理过期的限流器
    /**
     * ScheduledExecutorService 是 Java 并发包(java.util.concurrent)中的一个接口,
     * 它扩展了 ExecutorService,专门用于调度任务,支持延迟执行和周期性执行任务。
     * 它是 Java 中实现定时任务和周期性任务的标准方式。
     * 延迟执行任务:可以在指定的延迟时间后执行任务。
     * 周期性执行任务: 可以按照固定的时间间隔重复执行任务。
     */
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

    // 启动定时任务,定期清理过期的限流器
    LoginInterceptor() {
        startLimiterCleanupTask(TimeUnit.SECONDS);
    }

 /**
     * 启动定时任务,定期清理过期的限流器
     * 参数:
     * 第一个 1:定时任务的初始延迟时间(例如 1 分钟或 1 秒)。
     * 第二个 1:定时任务的执行间隔(例如每 1 分钟或每 1 秒执行一次)。
     * timeUnit:时间单位(由方法参数传入)。
     * long currentTime = System.currentTimeMillis(); 包含在定时器中的, 根据参数, 间隔重复执行, 所以时间也会动态的更新,
     */
    private void startLimiterCleanupTask(TimeUnit timeUnit) {
        scheduler.scheduleAtFixedRate(() -> {
            long currentTime = System.currentTimeMillis();
            ipRateLimiterMap.entrySet().removeIf(entry -> {
                TokenBucket limiter = entry.getValue();
                // 如果限流器超过过期时间未被使用,则移除
                // 当前系统时间 - 最后使用时间 > 过期时间(比如 5 分钟过期后)
                return currentTime - limiter.getLastAccessTime() > limiter.getExpireTime();
            });
        }, 1, 1, timeUnit); // 每分钟检查一次
    }

测试:  

将生成令牌的代码注释掉,  初始化设置令牌桶容量 10, 过期时间设置为 1 分钟 ,  当消耗 10 个令牌之后, 就无法继续执行后续的请求了,  此时等 1 分钟, 一分钟后, 继续请求, 就会发现又可以消耗令牌了. 

在过期时间内没有使用限流器,  就会将限流器删除,  下次请求来临时,  会为其重新分配令牌桶, 初始化获得了 10 个令牌, 所以才可以继续使用, 那么这个定时清除超时的限流器就实现了.


三、完整的限流器 + HandlerInterceptor 实现类代码 

LoginInterceptor implements HandlerInterceptor  :

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * Created with IntelliJ IDEA.
 * Description: 登录拦截器
 * Author: 保护小周
 * Date: 2023-08-07
 * Time: 17:39
 */
@Component
@Order(2)
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

    /**
     * 桶的容量
     */
    private int capacity = 10;

    /**
     * 每单位时间生成的令牌数
     */
    private long tokensPerUnit = 1l;

    /**
     * 阻塞队列的长度
     */
    private int queueSize = 10;

    // 存储每个 IP 地址的限流器
    private final ConcurrentHashMap<String, TokenBucket> ipRateLimiterMap = new ConcurrentHashMap<>();

    // 定时任务线程池,用于清理过期的限流器
    /**
     * ScheduledExecutorService 是 Java 并发包(java.util.concurrent)中的一个接口,
     * 它扩展了 ExecutorService,专门用于调度任务,支持延迟执行和周期性执行任务。
     * 它是 Java 中实现定时任务和周期性任务的标准方式。
     * 延迟执行任务:可以在指定的延迟时间后执行任务。
     * 周期性执行任务: 可以按照固定的时间间隔重复执行任务。
     */
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

    // 启动定时任务,定期清理过期的限流器
    LoginInterceptor() {
        startLimiterCleanupTask(TimeUnit.SECONDS);
    }

    /**
     * 预处理(请求的前置处理)回调⽅法<br/>
     * 返回值: <br/>true 流程继续;<br/>
     * false流程中断, 不会再调⽤其他的拦截器
     * @param request
     * @param response
     * @param handler
     * @return true 继续 false 中断
     * @throws Exception
     */
    @Override // 请求再到达服务器之前的前置处理
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 1. 获取客户端 IP 地址
        String clientIp = getClientIp(request);
        log.info(clientIp);

        // 获取或创建限流器,  ip 为 key , 限流器为 value
        // 工作原理 :
        // 如果 Map 中已经存在该 key,则返回对应的值,而不调用 mappingFunction。
        // 如果 Map 中不存在该 key,则调用 mappingFunction 来计算该 key 的值,然后将 key 和计算出来的值一起插入 Map 中。
        TokenBucket rateLimiter = ipRateLimiterMap.computeIfAbsent(clientIp, k ->
                new TokenBucket(capacity, TimeUnit.SECONDS, tokensPerUnit ,queueSize));

        // 动态设置过期时间(例如根据请求头或外部配置)
        long dynamicExpireTime = getDynamicExpireTime(); // 从请求中获取动态过期时间
        rateLimiter.setExpireTime(dynamicExpireTime);

        // 更新限流器的最后访问时间, 此时的限流器, 要么是根据 ip 新创建的, 要么是根据 ip 获取之前的, 设置最后等待时间正正好
        rateLimiter.setLastAccessTime(System.currentTimeMillis());

        // 2. 当请求来临时尝试获取令牌, return true / false
        if (!rateLimiter.tryAcquire()) {
            // 如果限流器拒绝请求,直接向客户端返回错误响应
            sendErrorResponse(response, 429, "请求过于频繁,请稍后重试");
            // 拒绝路由请求
            return false;
        }
        
        // 3. 用户身份认证, 判断当前用户是否是登录状态, 如果未登录, 拒绝, 否则放行 

        // 请求放行, 路由至指定 API
        return true;
    }

    /**
     * 获取过期时间
     * @return
     */
    private long getDynamicExpireTime() {
        return 1 * 60 * 1000;  // 1 分钟描述
    }

    /**
     * 发送错误响应(JSON 格式)
     */
    private void sendErrorResponse(HttpServletResponse response, int code, String message) throws IOException {
        response.setStatus(code);
        response.setContentType("application/json;charset=UTF-8");

        // 构建 JSON 响应体
        Map<String, Object> errorResponse = new HashMap<>();
        errorResponse.put("code", code);
        errorResponse.put("message", message);
        errorResponse.put("data", null);

        // 将 JSON 写入响应
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.writeValue(response.getWriter(), errorResponse);
    }

    /**
     * 获取客户端 IP 地址
     * @param request HttpServletRequest
     * @return 客户端 IP 地址
     */
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }

    /**
     * 启动定时任务,定期清理过期的限流器
     * 参数:
     * 第一个 1:定时任务的初始延迟时间(例如 1 分钟或 1 秒)。
     * 第二个 1:定时任务的执行间隔(例如每 1 分钟或每 1 秒执行一次)。
     * timeUnit:时间单位(由方法参数传入)。
     * long currentTime = System.currentTimeMillis(); 包含在定时器中的, 根据参数, 间隔重复执行, 所以时间也会动态的更新,
     */
    private void startLimiterCleanupTask(TimeUnit timeUnit) {
        scheduler.scheduleAtFixedRate(() -> {
            long currentTime = System.currentTimeMillis();
            ipRateLimiterMap.entrySet().removeIf(entry -> {
                TokenBucket limiter = entry.getValue();
                // 如果限流器超过过期时间未被使用,则移除
                // 当前系统时间 - 最后使用时间 > 过期时间(比如 5 分钟过期后)
                return currentTime - limiter.getLastAccessTime() > limiter.getExpireTime();
            });
        }, 1, 1, timeUnit); // 每分钟检查一次
    }
}

TokenBucket 限流器类

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * Author: 保护小周
 * Date: 2025-03-15
 * Time: 21:04
 */
public class TokenBucket {

    /**
     * 当前桶中的令牌数量
     */
    private long tokens;

    /**
     * 桶的容量
     */
    private final long capacity;

    /**
     * 时间单位
     */
    private final TimeUnit timeUnit;

    /**
     * 每单位时间生成的令牌数
     */
    private final long tokensPerUnit;

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

    /**
     * 请求队列,用于存储被限流的请求
     */
    private final LinkedBlockingQueue<Runnable> requestQueue;

    /**
     * 最后访问时间 (单位: 毫秒)
     */
    private long lastAccessTime;

    /**
     * 过期时间(单位:毫秒)
     */
    private long expireTime;


    /**
     * 初始化构造方法
     *
     * @param capacity      桶的容量
     * @param tokensPerUnit 每单位时间生成的令牌数
     * @param timeUnit      时间单位, 支持 TimeUnit 的所有时间单位(如 SECONDS、MINUTES、HOURS 等),使得令牌生成速率的配置更加灵活。
     * @param queueSize     队列的长度
     */
    public TokenBucket(int capacity, TimeUnit timeUnit, long tokensPerUnit, int queueSize) {
        this.capacity = capacity;
        this.tokensPerUnit = tokensPerUnit;
        this.timeUnit = timeUnit;
        this.lastRefillTime = System.currentTimeMillis();
        this.tokens = capacity;
        this.requestQueue = new LinkedBlockingQueue<>(queueSize);
        this.lastAccessTime = System.currentTimeMillis(); // 最后访问时间
        this.expireTime = 5 * 60 * 1000; // 默认过期时间:5 分钟

        // 启动一个后台线程处理队列中的请求
        startQueueConsumer();
    }


    // 核心 API

    /**
     * 尝试获取令牌
     *
     * @return true:成功获取令牌,允许请求通过;false:没有足够的令牌,拒绝请求
     */
    public synchronized boolean tryAcquire() {
        refillTokens(); // 补充令牌
        if (tokens > 0) {
            tokens--; // 消耗一个令牌
            return true; // 允许请求通过
        }
        return false; // 没有足够的令牌,拒绝请求
    }

    /**
     * 补充令牌
     */
    private void refillTokens() {
        long now = System.currentTimeMillis();

        // 计算距离上次补充令牌的时间间隔
        long elapsedTime = now - lastRefillTime;
        // 将时间差转换为时间单位
        // TimeUnit.MILLISECONDS 的作用是将时间间隔从毫秒转换为指定的时间单位(例如秒、分钟等),以便正确计算令牌的补充数量。
        // timeUnit , 是需要我们指定的时间单位, 秒, 分钟,小时....
        long elapsedUnits = timeUnit.convert(elapsedTime, TimeUnit.MILLISECONDS);

        if (elapsedUnits > 0) {
            // 计算这段时间内应该生成的令牌数量,   elapsedUnits, 就描述了几个单位时间
            long newTokens = elapsedUnits * this.tokensPerUnit;
            // 最多就一次性将桶装满了.
            this.tokens = Math.min(capacity, this.tokens + newTokens);
            // 保存本次补充令牌的时间戳, 用于下次补充令牌计算时间间隔
            this.lastRefillTime = now;
        }
    }


    /**
     * 处理任务
     *
     * @param request 需要处理的任务
     */
    public void handleRequest(Runnable request) {
        if (tryAcquire()) {
            // 令牌可用,直接处理请求
            request.run();

            if (tokens > 0 && !requestQueue.isEmpty()) { // 还有多的令牌,且队列不为空
                requestQueue.notify(); // 唤醒
            }
        } else {
            // 令牌不足,将请求放入队列
            synchronized (requestQueue) {
                if (requestQueue.offer(request)) {
                    System.out.println("Request added to queue");
                    requestQueue.notify(); // 通知消费者线程有新的请求
                } else {
                    // 队列已满,直接拒绝
                    System.out.println("Request rejected: Queue is full");
                }
            }
        }
    }


    /**
     * 启动队列消费者线程,处理队列中的请求
     */
    private void startQueueConsumer() {
        Thread consumerThread = new Thread(() -> {
            while (true) {
                synchronized (requestQueue) {
                    while (requestQueue.isEmpty()) {
                        try {
                            requestQueue.wait(); // 等待队列中有请求
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                    // 从队列中取出请求
                    Runnable request = requestQueue.poll();
                    if (request != null) {
                        // 尝试获取令牌
                        while (!tryAcquire()) {
                            try {
                                Thread.sleep(100); // 等待一段时间后重试, 可以根据令牌生产的速率进行调整
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        // 令牌可用,处理请求
                        request.run();
                    }
                }
            }
        });
        consumerThread.setDaemon(true); // 设置为守护线程
        consumerThread.start();
    }


    public long getExpireTime() {
        return expireTime;
    }

    public void setExpireTime(long expireTime) {
        this.expireTime = expireTime;
    }

    public long getLastAccessTime() {
        return lastAccessTime;
    }

    public void setLastAccessTime(long lastAccessTime) {
        this.lastAccessTime = lastAccessTime;
    }

}

改进点: 

  • 可以根据系统负载动态调整 tokensPerUnit,以应对流量波动。

  • 队列满时的拒绝策略. 

大家可以根据需要, 在该思想的基础之上做出不同的调整.  更多的功能就需要各自去实现啦, 我就写到这里了.


 好了,到这里,【令牌桶算法】博主已经分享完了,阐述较为基础,  希望对大家有所帮助,如有不妥之处欢迎批评指正。 

感谢每一位观看本篇文章的朋友,更多精彩敬请期待:保护小周ღ *★,°*:.☆( ̄▽ ̄)/$:*.°★* 

遇见你,所有的星星都落在我的头上……

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

保护小周ღ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值