使用HandlerInterceptor和RateLimiter进行限流

欢迎查看Eetal的第十九篇博客–使用HandlerInterceptor和RateLimiter进行限流

为什么使用拦截器

目前网上有博客使用的是aop,但是因为我项目里使用了shiro进行权限验证
shiro的过滤器链优先级是低于spring的过滤器的
所以根据约定优于配置,直接使用过滤器就不需要去配置shiro的一些映射和过滤器的优先级
同时网上博客基于aop为注解添加令牌桶效率的属性是有bug的,因为springMVC多线程是Method级别
而在aop的切面里创建rateLimiter或者为拿到的method对象的annotation对象绑定一个rateLimiter都会有线程问题
基于以上,把ratelimiter拿到拦截器,如果要定义不同rateLimiter可以改为定义多个拦截器和注解

常见限流算法

常见的限流方法有信号量计数器(Semaphore)计数当前线程个数
漏桶算法—使用一个容器保存进来的任务,按照固定速率流出任务,桶满时新加入的任务直接流出
令牌桶算法—使用一个容器存储令牌,按照固定速率生产令牌,令牌桶满时直接丢弃新生成令牌,任务进来以后尝试获取令牌,获取成功的任务开始执行
计数器较麻烦,需要维护实时记录线程数,完成时进行维护
对比令牌桶和漏桶,漏桶算法无法解决当容量空闲了一段时间以后,大量任务一起进来时,执行的平均效率低下的问题,而令牌桶的令牌生成速率是固定的

rateLimiter介绍

RateLimiter是guava提供的基于令牌桶算法的实现类
create(Double permitsPerSecond)方法根据给定的(令牌:单位时间(1s))比例为令牌生成速率
tryAcquire()方法尝试获取一个令牌,立即返回true/false,不阻塞,重载方法具备设置获取令牌个数、获取最大等待时间等参数
acquire()方法与tryAcquire类似,但是会阻塞,尝试获取一个令牌,没有时则阻塞直到获取成功

注解创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {

/**
* 获取令牌的等待时间 默认0
* @return
*/
int value() default 0;


/**
* 超时时间单位
* @return
*/
TimeUnit timeOutUnit() default TimeUnit.MILLISECONDS;

}

拦截器

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
public class RateLimitInterceptor implements HandlerInterceptor {

private final static Logger logger = LoggerFactory.getLogger(RateLimitInterceptor.class);

//每s产生2000个令牌
RateLimiter rateLimiter = RateLimiter.create(2000);

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
//如果是SpringMVC请求
if(handler instanceof HandlerMethod){
HandlerMethod handlerMethod = (HandlerMethod) handler;
RateLimit rateLimit = handlerMethod.getMethodAnnotation(RateLimit.class);
if(rateLimit != null) {
logger.info("rateLimit intercept method...");
if (!rateLimiter.tryAcquire(rateLimit.value(), rateLimit.timeOutUnit())) {
throw new Exception("系统繁忙,新稍后再试!");
//这里抛出异常是因为我项目里对异常进行了全局处理
}
}
}
return true;
}

}

springBoot拦截器配置

1
2
3
4
5
6
7
8
9
10
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RateLimitInterceptor()).addPathPatterns("/**")
.excludePathPatterns("/static/**", "/");
//配置限流拦截器拦截以及不拦截的映射
}
}

开始使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RateLimit(1000)	//最大等待时间1s
@RequestMapping("/login")
public ModelAndView login(User user) {
ModelAndView mv = new ModelAndView();
String username=user.getUsername();
String password=user.getPassword();
UsernamePasswordToken token = new UsernamePasswordToken(username,password,false);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
} catch(IncorrectCredentialsException e){
mv.addObject("msg", "密码错误");
mv.setViewName("/login.html");
return mv;
} catch (AuthenticationException e) {
mv.addObject("msg", "登录失败");
mv.setViewName("/login.html");
return mv;
}

mv.addObject("userDto",(UserDto)subject.getSession().getAttribute("userDto"));
mv.setViewName("/index.html");
return mv;
}

请移步

个人主页: yangyitao.top