高并发保护常用方案

  • 缓存:缓存的目的是提升系统访问速度和增大系统处理容量
  • 降级:降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行
  • 限流:限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理

为什么需要限流

在一个高并发系统中对流量的把控是非常重要的,当巨大的流量直接请求到我们的服务器上没多久就可能造成接口不可用,不处理的话甚至会造成整个应用不可用。

常用限流算法

  • 计数器法
  • 滑动窗口
  • 漏桶算法
  • 令牌桶算法

计数器法

方案一:第一个请求过来时,采用requestURI+userIDkeyvalue = 1,当第二个请求过来时incr自增,当缓存中的value达到请求最大数时直接拒绝访问该URI,弹出请求频繁提示。
方案二:第一种方案可用度很低,当存在多个接口需要限流而且每个接口承载的流量不一致时可用度不高,而且我们需要在访问接口前进行一个拦截处理,这里我们创建一个Interceptor(拦截器)并声明一个限流注解,既然这里使用了拦截器,我们可以在注解中判断是否需要用户登录,当方法上不存在限流注解时则返回true放行,然后进行计数器法进行限流。

限流注解

@Retention(RetentionPolicy.RUNTIME)
@Target(value = ElementType.METHOD)
public @interface AccessLimit {
    int second();
    int maxCount() default 5;
    boolean needLogin();
}

Interceptor

@Service
public class AccessInterceptor implements HandlerInterceptor {
    @Autowired
    private UserService userService;
    @Autowired
    private RedisService redisService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if(handler instanceof HandlerMethod) {
            User user = getMiaoshaUser(request);
            UserContext.setUser(user);

            HandlerMethod handlerMethod = (HandlerMethod) handler;
            AccessLimit methodAnnotation = handlerMethod.getMethodAnnotation(AccessLimit.class);
            if (methodAnnotation == null) {
                return true;
            }

            int second = methodAnnotation.second();
            int maxCount = methodAnnotation.maxCount();
            boolean needLogin = methodAnnotation.needLogin();

            if (needLogin) {
                if (user == null) {
                    render(response, CodeMsg.SESSION_ERROR);
                    return false;
                }
            }

            /**
             * 根据注解填的参数进行限流,接口的uri+"_"+userID为Key,最大请求数maxCount为Value
             *
             */
            String requestURI = request.getRequestURI();
            String key = requestURI + "_" + user.getId();

            AccessLimitKey accessLimitKey = AccessLimitKey.withExpire(second);

            Integer rMaxCount = redisService.get(accessLimitKey, key, Integer.class);
            if (rMaxCount == null) {
                redisService.set(accessLimitKey, key, 1);
            } else if (rMaxCount < maxCount) {
                redisService.incr(accessLimitKey, key);
            } else {
                System.out.println(CodeMsg.MIAOSHA_FREQUENTLY);
                render(response, CodeMsg.MIAOSHA_FREQUENTLY);
                return false;
            }
        }
            return true;
    }

    public void render(HttpServletResponse response, CodeMsg msg) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        String s = JSON.toJSONString(Result.error(msg));
        OutputStream os = response.getOutputStream();
        os.write(s.getBytes("UTF-8"));
        os.flush();
        os.close();
    }

    public User getMiaoshaUser(HttpServletRequest request){
        Cookie[] cookies = request.getCookies();

        if(cookies == null || cookies.length <=0){
            return null;
        }
        String token = null;
        for(Cookie cookie : cookies){
            if(cookie.getName().equals(LoginController.COOKIE_NAME)){
                //拿到token
                token = cookie.getValue();
                break;
            }
        }
        User user = userService.getUserByToken(token);
        return user;
    }
}

WebMvcConfigurer中注册拦截器

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(accessInterceptor);
    }

在拦截器中保存用户时使用的了ThreadLoal :每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用,因为拦截器会在参数解析器前先执行,所以在拦截器中获取到用户后可以在参数解析器中获取到User

public class UserContext {
    public static ThreadLocal<User> threadLocal = new ThreadLocal();

    public static void setUser(User user){
        threadLocal.set(user);
    }

    public static User getUser(){
        return threadLocal.get();
    }
}

这种方法有个最致命的弱点就是限制不精确:
在这里插入图片描述
如果某个接口限制的是一分钟最大请求为100,此时有一位狂徒张三在第0:59的情况下利用不正当工具请求的我们接口100次,然后在第1分钟的时候又请求了我们接口100次,这时就有了200/s的请求速,我们原则上是只承载1.6/s的请求速,整整大了125倍,通过在时间窗口的重置节点处突发请求,可以瞬间超过我们的速率限制。狂徒张三有可能通过算法的这个漏洞,瞬间压垮我们的应用,当然有其他算法可以避免这个漏洞,这里就不赘述。

下面我们来测试一下这个接口

    @AccessLimit(second = 5,maxCount = 5,needLogin = true)
    @RequestMapping(value = "/togoodslist",produces = "text/html")

5秒钟最多5个请求
当超过5个请求后拦截器直接拦截返回提示页面,避免频繁请求

最后修改:2021 年 03 月 24 日 02 : 42 PM
如果觉得我的文章对你有用,请随意赞赏