高并发处理之接口限流

最近开发的抢购活动上线后发现了两个比较明显的问题,其一:活动一开始,接口访问量剧增;其二:黑名单中增加了一大批黑名单用户(或者说IP),这其中就包含了一些恶意用户或机器人刷接口。

针对一些高并发的接口,限流是处理高并发的几大利剑之一。一方面,限流可以防止接口被刷,造成不必要的服务层压力,另一方面,是为了防止接口被滥用。

限流的方式也蛮多,本篇只讲几种我自己常用的,并且是后端的限流操作。


漏桶算法

漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。

漏桶算法示意图(图片取自网络)

漏桶算法可以很好地限制容量池的大小,从而防止流量暴增。


令牌桶算法

令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。

令牌桶算法示意图(图片取自网络)

令牌桶算法通过发放令牌,根据令牌的rate频率做请求频率限制,容量限制等。


自定义注解+拦截器+Redis实现限流

从代码层面来看,此方式实现还是比较优雅的,对业务层也没有太多的耦合。注意:此种方式单体和分布式均适用,因为用户实际的访问次数都是存在redis容器里的,和应用的单体或分布式无关。

@Inherited
@Documented
@Target({ElementType.FIELD,ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {
    //标识 指定sec时间段内的访问次数限制
    int limit() default 5;  
    //标识 时间段
    int sec() default 5;
}
public class AccessLimitInterceptor implements HandlerInterceptor {

    //使用RedisTemplate操作redis
    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;  

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            if (!method.isAnnotationPresent(AccessLimit.class)) {
                return true;
            }
            AccessLimit accessLimit = method.getAnnotation(AccessLimit.class);
            if (accessLimit == null) {
                return true;
            }
            int limit = accessLimit.limit();
            int sec = accessLimit.sec();
            String key = IPUtil.getIpAddr(request) + request.getRequestURI();
            Integer maxLimit = redisTemplate.opsForValue().get(key);
            if (maxLimit == null) {
                //set时一定要加过期时间
                redisTemplate.opsForValue().set(key, 1, sec, TimeUnit.SECONDS);  
            } else if (maxLimit < limit) {
                redisTemplate.opsForValue().set(key, maxLimit + 1, sec, TimeUnit.SECONDS);
            } else {
                output(response, "请求太频繁!");
                return false;
            }
        }
        return true;
    }

    public void output(HttpServletResponse response, String msg) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            outputStream.write(msg.getBytes("UTF-8"));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            outputStream.flush();
            outputStream.close();
        }
    }

    @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 {

    }
}
@Controller
@RequestMapping("/activity")
public class AopController {
    @ResponseBody
    @RequestMapping("/seckill")
    @AccessLimit(limit = 4,sec = 10)  //加上自定义注解即可
    public String test (HttpServletRequest request,@RequestParam(value = "username",required = false) String userName){
        //TODO somethings……
        return   "hello world !";
    }
}
/*springmvc的配置文件中加入自定义拦截器*/
<mvc:interceptors>
   <mvc:interceptor>
      <mvc:mapping path="/**"/>
      <bean class="com.pptv.activityapi.controller.pointsmall.AccessLimitInterceptor"/>
   </mvc:interceptor>
</mvc:interceptors>

访问效果如下,10s内访问接口超过4次以上就过滤请求,原理和计数器算法类似:


Guava的RateLimiter实现限流

guava提供的RateLimiter可以限制物理或逻辑资源的被访问速率,咋一听有点像java并发包下的Samephore,但是又不相同,RateLimiter控制的是速率,Samephore控制的是并发量。RateLimiter的原理就是令牌桶,它主要由许可发出的速率来定义,如果没有额外的配置,许可证将按每秒许可证规定的固定速度分配,许可将被平滑地分发,若请求超过permitsPerSecond则RateLimiter按照每秒 1/permitsPerSecond 的速率释放许可。注意:RateLimiter适用于单体应用。下面简单的写个测试:

<dependency>
   <groupId>com.google.guava</groupId>
   <artifactId>guava</artifactId>
   <version>23.0</version>
</dependency>
public static void main(String[] args) {
    String start = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
    RateLimiter limiter = RateLimiter.create(1.0); // 这里的1表示每秒允许处理的量为1个
    for (int i = 1; i <= 10; i++) { 
        limiter.acquire();// 请求RateLimiter, 超过permits会被阻塞
        System.out.println("call execute.." + i);
    }
    String end = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
    System.out.println("start time:" + start);
    System.out.println("end time:" + end);
}

可以看到,我假定了每秒处理请求的速率为1个,现在我有10个任务要处理,那么RateLimiter就很好的实现了控制速率,总共10个任务,需要9次获取许可,所以最后10个任务的消耗时间为9s左右。

放在Controller中用Jemter压测一下:

可以看到,模拟了20个并发请求,并设置了QPS为1,那么20个并发请求实现了限流的目的,后续的请求都要阻塞1s左右时间才能返回。要注意的是RateLimiter不保证公平性访问!


books 引申阅读: Guava Limiter实现限流

books 引申阅读: 使用quartz实现高级定制化定时任务(包含管理界面)


-----------------------------------------这又是一点分隔线 2018-08-16补充-----------------------------------

今天翻阅是发现使用上述方式使用RateLimiter的方式不够优雅,尽管我们可以把RateLimiter的逻辑包在service里面,controller直接调用即可,但是如果我们换成:自定义注解+AOP的方式实现的话,会优雅的多,详细见下面代码:

首先定义自定义注解

import java.lang.annotation.*;

/**
 * 自定义注解可以不包含属性,成为一个标识注解
 */
@Inherited
@Documented
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimitAspect {
   
}

自定义切面类

import com.google.common.util.concurrent.RateLimiter;
import com.simons.cn.springbootdemo.util.ResultUtil;
import net.sf.json.JSONObject;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
@Scope
@Aspect
public class RateLimitAop {

    @Autowired
    private HttpServletResponse response;

    private RateLimiter rateLimiter = RateLimiter.create(5.0); //比如说,我这里设置"并发数"为5

    @Pointcut("@annotation(com.simons.cn.springbootdemo.aspect.RateLimitAspect)")
    public void serviceLimit() {

    }

    @Around("serviceLimit()")
    public Object around(ProceedingJoinPoint joinPoint) {
        Boolean flag = rateLimiter.tryAcquire();
        Object obj = null;
        try {
            if (flag) {
                obj = joinPoint.proceed();
            }else{
                String result = JSONObject.fromObject(ResultUtil.success1(100, "failure")).toString();
                output(response, result);
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
        System.out.println("flag=" + flag + ",obj=" + obj);
        return obj;
    }
    
    public void output(HttpServletResponse response, String msg) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            outputStream.write(msg.getBytes("UTF-8"));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            outputStream.flush();
            outputStream.close();
        }
    }
}

测试controller

import com.simons.cn.springbootdemo.aspect.RateLimitAspect;
import com.simons.cn.springbootdemo.util.ResultUtil;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * 类描述:RateLimit限流测试(基于注解+AOP)
 * 创建人:simonsfan
 */
@Controller
public class TestController {

    @ResponseBody
    @RateLimitAspect
    @RequestMapping("/test")
    public String test(){
        return ResultUtil.success1(1001, "success").toString();
    }

这样通过自定义注解@RateLimiterAspect来动态的加到需要限流的接口上,个人认为是比较优雅的实现吧。

压测结果:

可以看到,10个线程中无论压测多少次,并发数总是限制在6,也就实现了限流,至于为什么并发数是6而不是5,我也很纳闷,这个问题在guava的github上提问了下,后面应该会有小伙伴解答的:https://github.com/google/guava/issues/3240


books 推荐阅读:elastic search搜索引擎实战demo:https://github.com/simonsfan/springboot-quartz-demo,分支:feature_es

books 推荐阅读:使用quartz实现定制化定时任务(包含管理界面)

相关推荐
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页