限流是应对高并发的策略之一,而使用Guava的RateLimiter能够方便快捷的实现API接口访问的限流。
RateLimiter特点:
使用:
首先引入依赖
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>28.2-jreversion>
dependency>
Limiter.java(限流注解)
import java.lang.annotation.*;
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Limiter {
double LimitNum() default 10; //默认每秒产生10个令牌
}
LimitException.java(自定义的限流异常类)
public class LimitException extends RuntimeException{
private Integer code;
private String msg;
public LimitException(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
//get和set省略
}
MyExceptionHandle.java(捕获自定义异常类,并进行处理)
@RestControllerAdvice
public class MyExceptionHandle {
private static final Logger LOG = LoggerFactory.getLogger(MyExceptionHandle.class);
@ExceptionHandler(LimitException.class)
public Object Handle(Exception e, HttpServletRequest request){
LOG.error("msg:{},url:{}", ((LimitException)e).getMsg(), request.getRequestURL());
Map<String, Object> map = new HashMap<>();
map.put("code",((LimitException) e).getCode());
map.put("msg",((LimitException) e).getMsg());
map.put("url", request.getRequestURL());
return map;
}
}
RateLimiterAspect.java(AOP切面,切入点是使用了上述LimitException注解的方法)
@Aspect
@Component
public class RateLimiterAspect {
//创建一个ConcurrentHashMap来存放各个方法和它们自己对应的RateLimiter对象
private static final ConcurrentMap<String, RateLimiter> RATE_LIMITER = new ConcurrentHashMap<>();
@Pointcut("@annotation(com.work.ohstudy.annotation.Limiter)")
public void rateLimit() {
}
private RateLimiter rateLimiter;
@Around("rateLimit()")
public Object pointcut(ProceedingJoinPoint point) throws Throwable {
Object obj = null;
//获取拦截的方法名
Signature sig = point.getSignature();
//获取拦截的方法名
MethodSignature msig = (MethodSignature) sig;
//返回被织入增加处理目标对象
Object target = point.getTarget();
//为了获取注解信息
Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
//获取注解信息
Limiter annotation = currentMethod.getAnnotation(Limiter.class);
double limitNum = annotation.LimitNum(); //获取注解每秒加入桶中的token
String functionName = msig.getName(); // 注解所在方法名区分不同的限流策略
if(RATE_LIMITER.containsKey(functionName)){
rateLimiter=RATE_LIMITER.get(functionName);
}else {
RATE_LIMITER.put(functionName,RateLimiter.create(limitNum));
rateLimiter=RATE_LIMITER.get(functionName);
}
if(rateLimiter.tryAcquire()) {
return point.proceed();
}
else {
throw new LimitException(500,"请求繁忙");
}
}
}
TestController.java(测试的Controller)
为了让测试效果直观,所以这里每秒产生令牌的速率设置成了0.01
@RestController
public class TestController {
@RequestMapping(value = "/test1")
@Limiter(LimitNum = 0.01)
public RetResult Curry(){
System.out.println("接口1请求成功");
return RetResponse.makeOKRsp(null);
}
@RequestMapping(value = "/test2")
@Limiter(LimitNum = 0.01)
public RetResult Harden(){
System.out.println("接口2请求成功!!");
return RetResponse.makeOKRsp(null);
}
}
测试1:
在浏览器输入http://localhost:8088/test1,并且刷新几次,之后再输入http://localhost:8088/test2,然后也刷新几次。
结果:
由上可以看出两个接口的限流是分开的。
测试2(测试RateLimiter令牌桶的预消费令牌):
先修改test1接口的令牌产生速率为每秒生成3个。
@RequestMapping(value = "/test1")
@Limiter(LimitNum = 3)
public RetResult Curry(){
System.out.println("接口1请求成功");
return RetResponse.makeOKRsp(null);
}
使用Postman测试,并发数10
结果:
可以看到明明每秒产生的令牌是3个,可这里请求却响应了4个,这就是之前说的预消费令牌。
把令牌产生速率改为每秒10个,并发数改为20,测试结果如下:
可以看出预消费令牌的数量也和每秒产生令牌的速率有关。在每秒生成3个时,预消费了1个令牌,而每秒生成10个时,预消费了3个令牌。