限流說詳細了,名堂也多。這種算法那種算法,這種策略那種策略的。沒有絕對的銀彈。都要結合實際的場景來實現。最簡單的,使用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");
}
}