SpringBoot基于guava集成令牌桶算法

一、什么是令牌桶

1、令牌桶

有一个固定大小的水桶,在水桶的水满之前,水龙头按照一定的频率恒定往里面滴水。水满之后就不滴了。
每次处理业务请求之前,都要先尝试从水桶里取出一滴水,才能处理业务。如果取不到,就不处理业务。
如此以来,桶的大小固定,水龙头滴水频率恒定,那么总的访问量也就恒定。从而也就保证了数据接口的访问流量,从而达到限流的目的。

2、功能图

在这里插入图片描述

二、Guava

1、简单介绍

guava是来自谷歌的Java核心类库。包含了大量Java工具类、集合类型、不可变集合、并发、I/O、hashing、缓存、原型、字符串等的通用功能。

2、pom引入

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>29.0-jre</version>
</dependency>

3、限速器

创建一个限速器,预热1秒,产生5个令牌
permitsPerSecond:每秒产生多少个令牌
warmupPeriod:预热时间
unit:预热时间warmupPeriod的单位

RateLimiter rateLimiter = RateLimiter.create(5, 1, TimeUnit.SECONDS);

尝试获取1个令牌,如果没有,会阻塞当前线程。直到获取成功返回。
返回值:阻塞的秒数。

double waitSeconds = rateLimiter.acquire();

尝试获取1个令牌,不会阻塞当前线程。
返回值:是否获取成功。

boolean success = rateLimiter.tryAcquire();

以上就是主要基础代码,创建限速器(生成水桶),(阻塞/不阻塞)获取令牌。

4、浅析重载方法

阻塞线程获取令牌

默认获取一个令牌。
public double acquire();
获取令牌数量手动传参,参数:permitspublic double acquire(int permits);

非阻塞线程获取令牌

参数说明如下
permits:获取令牌数量
timeout:等待时长
unit:时间单位

默认获取1个令牌,等待0秒,时间单位为微秒,其余同理
public boolean tryAcquire();
public boolean tryAcquire(int permits);
public boolean tryAcquire(Duration timeout);
public boolean tryAcquire(long timeout, TimeUnit unit);
public boolean tryAcquire(int permits, Duration timeout);
public boolean tryAcquire(int permits, long timeout, TimeUnit unit);

三、系统应用

1、单个接口应用

单接口测试如下

package com;

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

public class Test {

    private final RateLimiter rateLimiter;

    public Test(RateLimiter rateLimiter) {
        this.rateLimiter = rateLimiter;
    }

    public static void main(String[] args) {
        Test test = new Test(RateLimiter.create(1, 1, TimeUnit.SECONDS));
        for(int i = 0; i < 10; i++){
            rateLimiter(test.rateLimiter);
        }
    }

    private static void rateLimiter(RateLimiter rateLimiter){
        if(rateLimiter.tryAcquire()) {
            doBusiness();
            return;
        }
        System.out.println("服务器忙,请稍后重试!");
    }

    private static void doBusiness(){
        System.out.println("可以通行,处理业务");
    }

}

运行结果
在这里插入图片描述

2、多个接口应用

单接口可以处理之后,那么就需要用到一批接口上,做成配置的形式。
由于限流的本质就是对请求的拦截,那么自然而然就能想到使用拦截器处理。
限流处理的本质,类似于java中的切片,所以除了拦截器,也可以想到的是AOP,可以采用监听,使用注解等等方式。

那么就用拦截器先搞一搞吧,建一个拦截器如下

package com;

import com.alibaba.fastjson.JSON;
import com.Result;
import com.google.common.util.concurrent.RateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;

/**
 * 令牌桶算法拦截器
 */
@Component
@Slf4j
public class RateLimiterInterceptor extends HandlerInterceptorAdapter {

    private final RateLimiter rateLimiter;

    /**
     * 通过构造函数默认初始化限速器,创建一个限速器,预热1秒,产生10个令牌
     * permitsPerSecond:每秒产生多少个令牌
     * warmupPeriod:预热时间
     * unit:预热时间warmupPeriod的单位
     */
    public RateLimiterInterceptor() {
        super();
        this.rateLimiter = RateLimiter.create(5, 1, TimeUnit.SECONDS);
    }

    /**
     * 通过构造函数手动初始化限速器
     */
    public RateLimiterInterceptor(RateLimiter rateLimiter) {
        super();
        this.rateLimiter = rateLimiter;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if(this.rateLimiter.tryAcquire()) {
            //成功获取到令牌
            return true;
        }
        //获取失败,直接响应“错误信息”
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.getWriter().write(JSON.toJSONString(Result.error(400, "服务器繁忙,请稍后重试!")));
        return false;
    }

}

在WebMvcConfiguration中配置

package com.config;

@Slf4j
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

	@Autowired
    private RateLimiterInterceptor rateLimiterInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(rateLimiterInterceptor).addPathPatterns("/rate/limiter/**");
    }

}

测试Controller接口
注意:路径必须是 /rate/limiter

@Controller
@RequestMapping("/rate/limiter")
public class Test Controller {

    @Resource
    private ITest test;
    
    @RequestMapping("/api")
    @ResponseBody
    public Result doBusiness(@RequestBody String json) throws Exception {
        return test.doBusiness(json);
    }
	
}

效果同上。
加入每秒产生1个令牌,那在1秒内,只有第一次调用接口可以正常处理,其余接口均提示系统繁忙。

OK,整理到这吧!

如有不正确之处,还望指正!书写不易,觉得有帮助就点个赞吧!☺☺☺


版权声明:本文为qq_38254635原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。