在springboot中使用Guava基于令牌桶实现限流

限流说详细了,名堂也多。这种算法那种算法,这种策略那种策略的。没有绝对的银弹。都要结合实际的场景来实现。最简单的,使用Google的Guava,几行代码。就可以优雅的对一个接口完成限流

令牌桶算法

image

通俗的理解就是,有一个固定大小的水桶,水龙头一直按照一定的频率往里面滴水。水满了,就不滴了。客户端每次进行请求之前,都要先尝试从水桶里面起码取出“一滴水”,才能处理业务。因为桶的大小固定,水龙头滴水频率固定。从而也就保证了数据接口的访问流量。

Guava

谷歌的一个工具库,包含了大量的Java工具类,像hash算法,字符串处理,集合等等。。。

<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
	<groupId>com.google.guava</groupId>
	<artifactId>guava</artifactId>
	<version>29.0-jre</version>
</dependency>

速率限制器 RateLimiter

/**
 * 创建一个限速器,每1秒,产生2.5个令牌 
 */
RateLimiter rateLimiter = RateLimiter.create(2.5, 1, TimeUnit.SECONDS);

/**
 * 尝试获取1个令牌,如果没有,会阻塞当前线程。直到获取成功返回。
 * 返回值是,阻塞的秒数
 */
double waitSeconds = rateLimiter.acquire();

/**
 * 尝试获取1个令牌,不会阻塞当前线程。
 * 立即返回是否获取成功。
 */
boolean success = rateLimiter.tryAcquire();

好了,这就是核心代码。就3行。首先创建一个限速器,指定令牌的生产频率。
核心的方法就是2种,阻塞获取令牌,非阻塞获取令牌。代码也通俗易懂。

重载方法

不论是阻塞获取令牌还是非阻塞获取令牌,它们都有几个重载方法。一看也清楚,就是可以设置获取令牌的数量,以及阻塞的时间。

public double acquire(int permits)

public boolean tryAcquire(Duration timeout)
public boolean tryAcquire(int permits)
public boolean tryAcquire(long timeout, TimeUnit unit)
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) 
public boolean tryAcquire(int permits, Duration timeout)

Controller ,也就是被限速的接口

@RestController
@RequestMapping("/test")
public class TestController {
	
	@GetMapping
	public Object test () {
		return Collections.singletonMap("success", "true");
	}
}

RateLimiterInterceptor,负责实现限速逻辑

import java.nio.charset.StandardCharsets;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.http.MediaType;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

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

public class RateLimiterInterceptor extends HandlerInterceptorAdapter {

	private final RateLimiter rateLimiter;

	/**
	 * 通过构造函数初始化限速器
	 */
	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.setCharacterEncoding(StandardCharsets.UTF_8.name());
		response.setContentType(MediaType.TEXT_PLAIN_VALUE);
		response.getWriter().write("服务器繁忙");
		return false;
	}
}

拦截器的配置

import java.util.concurrent.TimeUnit;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

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

import io.springboot.jwt.web.interceptor.RateLimiterInterceptor;


@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
	
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		/**
		 * test接口,1秒钟生成1个令牌,也就是1秒中允许一个人访问
		 */
		registry.addInterceptor(new RateLimiterInterceptor(RateLimiter.create(1, 1, TimeUnit.SECONDS)))
			.addPathPatterns("/test");
	}
	
}

客户端演示限流效果

GIF

2 Likes

这样是只能针对全局接口的限流,如果是针对部分接口做不同qps限流,其实可以通过自定义注解,然后写个aspcet,应该可以更加灵活

1 Like

谢谢91凯文老师的分享 :wink:

为啥叫91 凯文老师
开车贼快吗?

别乱特么叫 :roll_eyes:

呜呜呜呜呜呜呜