【前情提要】前面说过目前几种比较常见的限流的中间件,Sentinel、Hystrix和resilience4j,也提到过自己实现限流功能,今天就基于Guava实现一哈限流功能。
壹、Guava介绍
Guava 是一种基于开源的Java库,其中包含谷歌正在由他们很多项目使用的很多核心库。这个库是为了方便编码,并减少编码错误。这个库提供用于集合,缓存,支持原语,并发性,常见注解,字符串处理,I/O和验证的实用方法。
Guava 的好处
- 标准化 - Guava库是由谷歌托管。
- 高效 - 可靠,快速和有效的扩展JAVA标准库
- 优化 -Guava库经过高度的优化。
- 函数式编程 -增加JAVA功能和处理能力。
- 实用程序 - 提供了经常需要在应用程序开发的许多实用程序类。
- 验证 -提供标准的故障安全验证机制。
- 最佳实践 - 强调最佳的做法。
下面就使用Guava 中提供的并发相关的工具中的RateLimiter
来实现一个限流的功能。
贰、引入依赖
1 2 3 4 5
| dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>30.0-jre</version> </dependency>
|
叁、拦截器方式实现
3.1、 定义接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @RequestMapping("/get") @ResponseBody public JsonResult allInfos(HttpServletRequest request, HttpServletResponse response, @RequestParam Integer num){ log.info("param----->" + num); try { Thread.sleep(num*100); if (num % 3 == 0) { log.info("num % 3 == 0"); throw new BaseException("something bad whitch 3", 400); }
if (num % 5 == 0) { log.info("num % 5 == 0"); throw new ProgramException("something bad whitch 5", 400); } if (num % 7 == 0) { log.info("num % 7 == 0"); int res = 1 / 0; } return JsonResult.ok(); } catch (ProgramException | InterruptedException exception) { log.info("error"); return JsonResult.error("error"); } }
|
3.2、 添加拦截器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| package com.eelve.limiting.guava.aspect;
import com.eelve.limiting.guava.vo.JsonResult; import com.google.common.util.concurrent.RateLimiter; import org.springframework.http.MediaType; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.nio.charset.StandardCharsets;
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.APPLICATION_JSON_VALUE); response.getWriter().write(JsonResult.error().toString()); return false; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| package com.eelve.limiting.guava.configuration;
import com.eelve.limiting.guava.aspect.RateLimiterInterceptor; import com.google.common.util.concurrent.RateLimiter; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.concurrent.TimeUnit;
@Configuration public class WebMvcConfiguration implements WebMvcConfigurer {
@Override public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RateLimiterInterceptor(RateLimiter.create(1, 1, TimeUnit.SECONDS))) .addPathPatterns("/get"); }
}
|
通过上面的代码我们就可用对/get
接口实现限流了,但是也有明显的缺点,就是规则被写死,所以下面我们通过注解方式实现。
肆、使用注解实现
4.1、定义注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| package com.eelve.limiting.guava.annotation;
import java.lang.annotation.*; import java.util.concurrent.TimeUnit;
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface MyRateLimiter { int NOT_LIMITED = 0;
String name() default "";
double qps() default NOT_LIMITED;
int timeout() default 0;
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
int executeTimeout() default 0;
TimeUnit executeTimeUnit() default TimeUnit.MILLISECONDS; }
|
4.2、添加通知
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| package com.eelve.limiting.guava.aspect;
import com.eelve.limiting.guava.annotation.MyRateLimiter; import com.eelve.limiting.guava.exception.BaseException; import com.google.common.util.concurrent.RateLimiter; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.stereotype.Component;
import java.lang.reflect.Method; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap;
@Slf4j @Aspect @Component public class MyRateLimiterAspect { private static final ConcurrentMap<String, RateLimiter> RATE_LIMITER_CACHE = new ConcurrentHashMap<>();
@Pointcut("@annotation(com.eelve.limiting.guava.annotation.MyRateLimiter)") public void MyRateLimit() {
}
@Around("MyRateLimit()") public Object pointcut(ProceedingJoinPoint point) throws Throwable { Object obj =null; MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); MyRateLimiter myRateLimiter = AnnotationUtils.findAnnotation(method, MyRateLimiter.class); if (myRateLimiter != null && myRateLimiter.qps() > MyRateLimiter.NOT_LIMITED) { double qps = myRateLimiter.qps(); String name = myRateLimiter.name(); int executeTimeout = myRateLimiter.executeTimeout(); if(Objects.isNull(name)){ name = method.getName(); } if (RATE_LIMITER_CACHE.get(name) == null) { RATE_LIMITER_CACHE.put(name, RateLimiter.create(qps)); }
log.debug("【{}】的QPS设置为: {}", method.getName(), RATE_LIMITER_CACHE.get(name).getRate()); Long start = System.currentTimeMillis(); if (RATE_LIMITER_CACHE.get(method.getName()) != null && !RATE_LIMITER_CACHE.get(method.getName()).tryAcquire(myRateLimiter.timeout(), myRateLimiter.timeUnit())) { throw new BaseException("请求频繁,请稍后再试~"); } obj = point.proceed();
Long end = System.currentTimeMillis(); Long executeTime = end - start; if((end - start) > executeTimeout){ log.debug("请求超时,请稍后再试~" + (end - start)); throw new BaseException("请求超时,请稍后再试~"); } } return obj; }
}
|
4.3、使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
@MyRateLimiter(qps = 2.0, timeout = 1) @GetMapping("/rateLimiter") public JsonResult rateLimiter(HttpServletRequest request, HttpServletResponse response, @RequestParam Integer num) { log.info("param----->" + num); try { Thread.sleep(num); } catch (InterruptedException e) { e.printStackTrace(); } log.info("【rateLimiter】被执行了。。。。。"); return JsonResult.ok("你不能总是看到我,快速刷新我看一下!"); }
|
通过上面的代码我们就实现了零活的通过注解的方式实现了限流功能,并且我们还可以在Around
通知的时候灵活实现。包括过滤某些异常等等。
【后面的话】除了前面我们使用的RateLimiter
之外,Guava
还提供了专门针对超时的SimpleTimeLimiter
组件,有兴趣的也可以尝试一下。另外以上的源码都可用在 limiting 中找到。