1.限流的原理,根据ip(也可以对总的访问频次做限制)和固定时间内的访问次数,做限制
2.springboot实现限流:
1)新建一个自定义注解:AccessLimit 用于接口上(注解实现更简单方便)
@Inherited
@Documented
@Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {
/**
* 指定second 时间内,API最多的请求次数
*/
int times() default 3;
/**
* 指定时间second,redis数据过期时间
*/
int second() default 10;
}
2)新建一个拦截器,获取自定义注解上的参数,然后缓存到redis中做校验
@Component
public class AccessLimitInterceptimplements HandlerInterceptor {
private static final Loggerlogger = LoggerFactory.getLogger(AccessLimitIntercept.class);
@Autowired
private RedisUtilredisUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler)throws Exception{
try{
// handler 是否为 HandlerMethod 实例
if(handlerinstanceof HandlerMethod){
// 强转
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 获取方法
Method method = handlerMethod.getMethod();
// 判断方法是否有AccessLimit注解,有的才需要做限流
if(!method.isAnnotationPresent(AccessLimit.class)){
return true;
}
// 获取注释上的内容
AccessLimit accessLimit = method.getAnnotation(AccessLimit.class);
if(accessLimit ==null){
return true;
}
// 获取方法上的请求次数
int times = accessLimit.times();
// 获取方法注释上的请求时间
Integer second = accessLimit.second();
// 拼接redis key = ip + api限流
String key = IpUtils.getIpAddr(request) + request.getRequestURI();
Integer maxTimes =null;
// 获取 redis 的 value
String value =redisUtil.get(key) ==null?"":redisUtil.get(key).toString();
if(!"".equals(value)){
maxTimes = Integer.valueOf(value);
}
if(maxTimes ==null){
// 如果redis中没有该ip对应的时间则表示第一次调用,保存key到redis
redisUtil.set(key,"1",second, TimeUnit.SECONDS);
}else if (maxTimes < times){
// 如果 redis 中的时间比注解上的时间小,则表示可以允许访问,这是修改redis的value时间
redisUtil.set(key,maxTimes +1 +"",second,TimeUnit.SECONDS);
}else {
logger.info(key +"请求过于频繁");
return setResponse(Result.error(ResultStatus.REQUEST_SEND_FREQUENTLY_ERROR.getStatus(),"请求过于频繁,请稍后重试"),response);
}
}
}catch (Exception ex){
logger.error("API请求限流拦截异常,异常原因:" + ex);
throw new ParameterException(ex.getMessage());
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)throws Exception{
}
@Override
public void afterCompletion(HttpServletRequest request,HttpServletResponse response,Object handler,Exception ex)throws Exception{
}
private boolean setResponse(Result result,HttpServletResponse response)throws IOException{
ServletOutputStream outputStream =null;
try{
response.setHeader("Content-Type","application/json;charset=utf-8");
outputStream = response.getOutputStream();
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
}catch (Exception ex){
logger.error("setResponse方法报错",ex);
return false;
}finally {
if(outputStream !=null){
outputStream.flush();
outputStream.close();
}
}
return false;
}
}
3)配置拦截器
@Configuration
public class WebFilterConfig implements WebMvcConfigurer {
/**
* 这里需要先将限流拦截器入住,不然无法获取到拦截器中的redistemplate
* @return
*/
@Bean
public AccessLimitIntercept getAccessLimitIntercept() {
return new AccessLimitIntercept();
}
/**
* 多个拦截器组成一个拦截器链
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(getAccessLimitIntercept()).addPathPatterns("/**");
}
}
4)在接口上添加注解,设置固定秒数内,限制的访问次数
@RestController
@RequestMapping("/")
public class PingController extends BaseController {
@AccessLimit(times = 5, second = 10)
@GetMapping(value = "/ping")
public Results ping() {
return succeed("pong", "");
}
}
5)注意:该限流加完后,接口依然可以被访问,如果需要修改为接口不可被访问,需要修改2)中setResponse()方法的放回值为false,preHandle()方法的返回值,可以决定方法是否可被执行,当为false时,方法不会被执行