限流算法

2020-03-06  本文已影响0人  愤怒的老照

没有单位时间访问量要求

这种不需要考虑时间,只需要考虑线程安全就可以,用信号量可以解决

// 最大访问量为100;
    int maxRequestNum = 100;
    Semaphore handler = new Semaphore(maxRequestNum);

    public void request(){
        if (!handler.tryAcquire()){
            return ;
        }

        // 调用接口
    }

QPS限流(是一段时间内(一般指1秒)的请求个数。)

计数器法

这种方法是QPS中最简单的,以1秒为时间间隔,同一秒内请求数小于最大限制就可以执行,否则不执行,下一秒后清空请求数即可

// 最大访问量为100;
    int maxRequestNum = 100;
    Semaphore handler = new Semaphore(maxRequestNum);
    long timestap = System.currentTimeMillis(); // 每个时间段的第一个时刻
    long interval = 1000; // 时间间隔1000ms

    public void request(){
       long now = System.currentTimeMillis();
       if (now < timestap + interval){
           if (handler.tryAcquire()){
               // 执行业务
           }else {
               return ;
           }
       }else {
           // 初始化下一个时间段
           timestap = now;
           handler = new Semaphore(maxRequestNum);
           // 执行业务
       }
    }

这种方法的缺点也是很明显的,粒度比较粗,假如一秒内的最大访问请求数量为100个,用户在第一毫秒就将这些请求全部使用,则这一秒内剩下的请求将全部不处理。

滑动窗口

1.png

这个是网上找的一个图,滑动窗口的思想就是将粒度细化,比如上图将1秒分为4分,每过0.25秒窗口像右滑动1/4,也就是每0.25秒腾出对应的访问数量。假如每秒最大访问量为100次,第1秒的后0.75秒和第二秒的前0.25秒访问量共100次,用计数法第二秒的后0.75秒将无法访问,如果使用滑动窗口的话,将会清空第一窗口的数量,使系统能再次访问。

    // 最大访问量为100;
    int maxRequestNum = 100;
    AtomicInteger[] handlers = new AtomicInteger[4];
    AtomicInteger sum;
    int index = 0;
    public void init(){
        // 初始化
        Arrays.stream(handlers)
                .map(item -> {
                   return new AtomicInteger(0);
                });
        sum = new AtomicInteger(0);
    }

    public synchronized void request(){
        // 对应窗口的访问量++;
        handlers[index].incrementAndGet();
        if (sum.incrementAndGet() < maxRequestNum){
            // 执行业务
        }else {
            // 限流
        }
    }

    //窗口滑动,每0.25秒执行一次
    public void slide(){
        // 窗口滑动
        index = (index + 1) % handlers.length;
        //对应窗口数量清空
        int count = handlers[index].getAndSet(0);
        // 窗口滑动,整体数减去窗口对应的数量
        sum.addAndGet(count);
    }

漏桶算法

2.png

这种方法是将水(请求)滴到桶中,桶中的水(请求)以固定速率漏出去,当水溢出来时,不处理请求,桶没有满时,处理请求。因为这种方法是固定速率的,所以没有计数器法中的边界。我们将算法中的水换成实际应用中的请求,我们可以看到漏桶算法天生就限制了请求的速度。

long timestamp = System.currentTimeMillis();
    int capacity; // 桶的容量
    int rate; // 水流出来的速率
    long water; //桶中的水数量

    public void request(){
        long now = System.currentTimeMillis();
        // 速率固定,可以使用相减来获取水的数量
        water = Math.max(0, water - (now - timestamp) * rate);
        if (water < capacity){
            water ++;
            // 执行请求
        }else {
            // 限流
        }
    }

令牌桶

3.png

为啥俺感觉这俩作用是一样的呢?网上说的令牌桶算法可以解决突发访问难道是加一个缓冲区???我来贴解释了https://www.cnblogs.com/xrq730/p/11025029.html
,可以使用Guava-RateLimiter

    long timestamp = System.currentTimeMillis();
    int capacity; //令牌桶容量
    int  rate; // 放入令牌的速率
    long sum; //当前令牌的数量

    public void request(){
        long now = System.currentTimeMillis();
        sum = Math.min(capacity, sum + (now - timestamp) * rate);
        timestamp = now;
        if (sum < 1){
            // 限流
        }else {
            sum -= 1;
            // 执行业务
        }
    }

实战

为了方便使用,通常封装成注解,使用切面来完成

Limit注解:
@Inherited
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Limit {
}

切面类
@Aspect
@Component
@Slf4j
public class LimitAspect {

    private static final int limit = 100;
    private ConcurrentHashMap<String, RateLimiter> handler = new ConcurrentHashMap<>();

    @Pointcut("@annotation(com.whut.insurance.common.annotation.Limit)")
    public void pointcut() {
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) {
        HttpServletResponse response = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getResponse();
        // 获取许可。如果有返回true,否则返回false
        Signature sig = joinPoint.getSignature();
        MethodSignature msig = (MethodSignature) sig;
        Object target = joinPoint.getTarget();
        boolean token = false;
        try {
            Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
            if (handler.containsKey(currentMethod.getName())){
                token = handler.get(currentMethod.getName()).tryAcquire();
            }else {
                RateLimiter rateLimiter =  RateLimiter.create(limit);
                handler.put(currentMethod.getName(), rateLimiter);
                token = rateLimiter.tryAcquire();
            }
        } catch (NoSuchMethodException e) {
            log.error("get method error[{}]", e.getMessage());
        }
        System.out.println(handler.toString());
        Object obj = null;
        try {
            if (token) {
                obj = joinPoint.proceed();
            } else {
                ResponseWriter.println(response, ServerResponse.createByErrorCodeMessage(
                        ResponseCode.FREQUENT_VISIT.getCode(),
                        ResponseCode.FREQUENT_VISIT.getDesc()
                ));
            }
        } catch (Throwable e) {
            log.error("get method error[{}]", e.getMessage());
        }
        return obj;
    }

}

使用
    @Limit
    @PostMapping("/captcha")
    public void captcha(@RequestBody @Valid RandomKeyParam randomKey, HttpServletResponse response) throws IOException, IOException {}
上一篇 下一篇

猜你喜欢

热点阅读