高并发处理之接口限流

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

针对一些高并发的接口,限流是处理高并发的几大利剑之一。一方面,限流可以防止接口被刷,造成不必要的服务层压力,另一方面,是为了防止接口被滥用。
限流的方式也蛮多,本篇只讲几种我自己常用的,并且是后端的限流操作。

漏桶算法

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

漏桶算法示意图(图片取自网络)
漏桶算法可以很好地限制容量池的大小,从而防止流量暴增。

令牌桶算法

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


令牌桶算法示意图(图片取自网络)
令牌桶算法通过发放令牌,根据令牌的rate频率做请求频率限制,容量限制等。

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

从代码层面来看,此方式实现还是比较优雅的,对业务层也没有太多的耦合。注意:此种方式单体和分布式均适用
@Inherited
@Documented
@Target({ElementType.FIELD,ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {

    int limit() default 5;  

    int sec() default 5;
}
public class AccessLimitInterceptor implements HandlerInterceptor {

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

    @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) {
                redisTemplate.opsForValue().set(key, 1, sec, TimeUnit.SECONDS);  //set时一定要加过期时间
            } 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不保证公平性访问!

----------------------------------------这是一条分隔线 2018-06-14补充---------------------------------------

针对实际项目使用RateLimiter来限流,正确的打开方式应该还有更好的方式,对于这个问题,我在github上提问了,并得到了相应的解答,问题讨论区在这里:https://github.com/google/guava/issues/3180

猜你喜欢

转载自blog.csdn.net/fanrenxiang/article/details/80683378