背景
日常开发过程中,经常需要对一些资源消耗大的接口(例如:图片上传/下载接口,当遇到并发量大的时候,网络带宽占比不断增加,导致程序无法正常运行)进行限流操作。以此来增加应用的稳定性和安全性。
解决方案
基于拦截器,在网关层对请求进行拦截,利用redis存贮请求来源信息,并进行计数,超过阀值时直接拒绝请求。利用注解来标记需要拦截的方法,方便灵活。(此方案基于springboot+redis)
定义注解类Semaphore
@Documented //生成文档
@Inherited //可以被子类使用
@Retention(RetentionPolicy.RUNTIME) //运行时可以使用
@Target({ElementType.METHOD, ElementType.TYPE}) //目标元素
public @interface Semaphore {
}
定义拦截器PassportInterceptor
@Component
public class PassportInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (handler instanceof HandlerMethod) {//仅针对方法
RequestLimitConfigconfig = SpringUtils.getBean(RequestLimitConfig.class);//自定义配置类 封装限流相关属性
HandlerMethod methodHandler = (HandlerMethod) handler;
Method method = methodHandler.getMethod();
//匹配用到了该注解的方法或者类
Semaphore nm = method.getAnnotation(Semaphore.class);
Semaphore nc = method.getDeclaringClass().getAnnotation(Semaphore.class);
// 是否开启拦截 配置文件中定义 是否开启拦截
if (!config.getSwitchs()) {
return true;
}
String uri = getUri();
if (nm == null && nc == null) {
return true;
}
// IP级别限流
Integer qps = config.getQps();
boolean check = SemaphoreUtil.check(CommonUtils.getIpAddress(), uri, qps);
if (!check) {
response.setStatus(ResultCode.REQUEST_TOO_FREQUENT.getCode());
return false;
}
// URL级别限流
qps = config.getAllqps();
check = SemaphoreUtil.check(uri, qps);
if (!check) {
response.setStatus(ResultCode.REQUEST_TOO_FREQUENT.getCode());
return false;
}
}
return true;
}
private String getUri() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
String uri = request.getRequestURI();
if (uri.startsWith("/")) {
uri = uri.substring(1, uri.length());
}
return uri.replaceAll("/", ".");
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
LOGGER.info("调用postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
LOGGER.info("调用afterCompletion");
}
}
配置拦截器
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Autowired
private PassportInterceptor passportInterceptor;
//配置拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
//可配置多个
registry.addInterceptor(passportInterceptor).addPathPatterns("/**");
}
}
定义工具类 测试时可将参数ALL_TIMES 适当调大
public class SemaphoreUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(SemaphoreUtil.class);
private static final JRedisClient redisClient_01 = JRedisClient.getInstance();
public static int TIMES = 1;
public static int ALL_TIMES = 1;//表示限流单位时间为1秒
/**
* IP限流检查
*
* @param ip
* @param url
* @return
*/
public static boolean check(String ip, String url, Integer qps) {
if (qps == null || qps == 0) {
return true;
}
String key = "CURRENT_INVOK" + "_" + ip + "_" + url;
long count = RedisUtil.incr(key);
if (count == 1) {
RedisUtil.expire(key, ALL_TIMES);
} else if (count > qps) {
LOGGER.info("当前key:" + key + "计数为:" + count + ",网关拒绝请求");
return false;
}
return true;
}
/**
* URL限流检查
*
* @param url
* @param qps
* @return
*/
public static boolean check(String url, Integer qps) {
if (qps == null || qps == 0) {
return true;
}
String key = "CURRENT_INVOK" + "_" + url;
long count = RedisUtil.incr(key);
if (count == 1) {
RedisUtil.expire(key, ALL_TIMES);
} else if (count > qps) {
LOGGER.info("接口级别当前key:" + key + "计数为:" + count + ",网关拒绝请求");
return false;
}
return true;
}
}
注解在接口上使用
@RequestMapping(value = "/upload", method = RequestMethod.POST)
@ResponseBody
@Semaphore
public Result uploadFile(@RequestParam("file") MultipartFile file) {}