1. 程式人生 > 實用技巧 >在springboot中使用Guava基於令牌桶實現限流

在springboot中使用Guava基於令牌桶實現限流

限流說詳細了,名堂也多。這種演算法那種演算法,這種策略那種策略的。沒有絕對的銀彈。都要結合實際的場景來實現。最簡單的,使用Google的Guava,幾行程式碼。就可以優雅的對一個介面完成限流

令牌桶演算法

通俗的理解就是,有一個固定大小的水桶,水龍頭一直按照一定的頻率往裡面滴水。水滿了,就不滴了。客戶端每次進行請求之前,都要先嚐試從水桶裡面起碼取出“一滴水”,才能處理業務。因為桶的大小固定,水龍頭滴水頻率固定。從而也就保證了資料介面的訪問流量。

Guava

谷歌的一個工具庫,包含了大量的Java工具類,像hash演算法,字串處理,集合等等。。。

https://github.com/google/guava

<!-- 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");
	}
	
}

客戶端演示限流效果


原文:https://springboot.io/t/topic/2352