微服务中常用的限流算法(一)

一个业务系统在指定配置的服务器上,可以承载的容量是一定的,当请求流量超过系统的容量后,系统就会变得不稳定,可用性下降,为了保证系统的可用性,需要将系统能够承载容量之外的流量进行丢弃,这样虽然会导致部分用户请求失败,但是整个系统依然是可用的,依然能对外提供服务,而不是因为负载压力太大导致整个系统崩溃,使所有用户都不能访问,这就是保证系统高可用的限流方案。

目前业内实现限流的算法有四种,分别是固定窗口算法,滑动窗口算法,漏桶算法和令牌桶算法。

为方面下文代码实现,我们定义了接口和对应的抽闲类:
1.接口定义:

public interface LimitAlgo {
	// 获取限流资源
    boolean tryAcquire();
}

2.抽象类定义

public abstract class AbstractLimitAlgo implements LimitAlgo{

	// 资源限制
    protected int limit;
    
    // 最近获取限流资源时间戳
    protected long lastTimeStamp;

    public AbstractLimitAlgo(int limit){
        this.limit = limit;
        this.lastTimeStamp = System.currentTimeMillis();
    }

    protected long getElapseTimeFromLast() {
        return System.currentTimeMillis() - lastTimeStamp;
    }
}

固定窗口算法

固定窗口算法是一个相对比较简单的限流算法,该算法是将一个固定的时间单元当做一个时间窗口,每个窗口仅允许限制的流量内的请求通过,具体如下图:

在这里插入图片描述

我们将是事件线切分成一个一个的限流窗口,每个限流窗口有一个窗口的开始和结束时间,窗口开始时,计数器清零,在这个窗口的时间范围内,每进来一个请求,计数器+1,如果计数器记录值超过限流阈值,就拒绝服务,直接给客户端响应503。当限流窗口结束后,进入下一个限流窗口,计数器再次清零,重新开始计数。
固定窗口的限流算法的核心代码实现如下:

public class FixedWindow extends AbstractLimitAlgo {

    private long windowLength;
    private AtomicInteger counter = new AtomicInteger(0);
    private Lock lock = new ReentrantLock();

    public FixedWindow(int limit,long windowLength) {
        super(limit);
        this.windowLength = windowLength;
    }

    @Override
    public boolean tryAcquire() {
        long elapsedTimeStamp = getElapseTimeFromLast();
        if(elapsedTimeStamp >= windowLength) {
            lock.lock();
            try{
                if(getElapseTimeFromLast() >= windowLength) {
                    lastTimeStamp = System.currentTimeMillis();
                    counter.getAndSet(0);
                }
            }catch (Exception e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
        return counter.incrementAndGet() <= limit;
    }
}

滑动窗口算法实思想比较简单,但是限流效果比较差,会出现 2倍配置速率问题,导致无法达到限流的目的。简单来说,当限流算法进入一个新的窗口后,计数器会重新计数,我们假设每个窗口限流100个请求,在第一个限流窗口快要结束时,突然进入进入100个请求,因为这个请求量在限流范围内,所以没有触发限流,请求全部通过,然后进入第二个限流窗口,计数器重新计数,这时又忽然进来100个请求,因为此时在第二个限流窗口,所以也没有触发限流,这就导致在短时间内进入了200个请求,这样会给系统造成巨大的负载压力。具体示意图如下:
在这里插入图片描述

滑动窗口

为了解决固定窗口2倍速率配置的问题,我们可以采用滑动窗口限流。滑动窗口之所以可以解决固定窗口2倍速率配置的问题,是因为滑动窗口将限流窗口划分成多个更细粒度的分片单元,滑动窗口每次只滑动一个分片单元,短时间出现的请求仍然在一个滑动窗口内,仍然可以被滑动窗口限流,也就是说,前一个滑动窗口的请求数量可以对下一个滑动窗口产生影响。而固定窗口的两个窗口之间完全没有任何关系,这也是固定窗口产生2倍速率配置问题的根本原因。滑动窗口示意图如下:
在这里插入图片描述

滑动窗口的实现方式和固定窗口基本是一致的,只不过要改动重置“窗口计数器”和“当前窗口结束时间”的逻辑就可以了,在重置窗口结束时间方面:固定窗口算法将窗口结束时间置为+1 窗口长度,而滑动窗口算法将窗口结束时间为+1时间片长度。在重置计数器方面:固定窗口算法直接将计数器置为0,而滑动窗口则是将计数器置为窗口包含时间片中所有请求之和。

滑动窗口实现代码如下:

public class SlideWindow extends AbstractLimitAlgo {

    private long windowLength;
    private AtomicInteger[] windowPlice;
    private AtomicInteger counter = new AtomicInteger(0);
    private Lock lock =  new ReentrantLock();
    private volatile int index;
    private int newCounter;

    public SlideWindow(int limit, long windowLength, int unitCnt) {
        super(limit);
        this.windowLength = windowLength;
        this.lastTimeStamp = System.currentTimeMillis();
        this.windowPlice = new AtomicInteger[unitCnt];
        for(int i=0;i<unitCnt;i++) {
            windowPlice[i] = new AtomicInteger(0);
        }
    }

    @Override
    public boolean tryAcquire() {
        long unitLength = windowLength / windowPlice.length;
        long elapseTimeStamp = getElapseTimeFromLast();
        if(elapseTimeStamp >= unitLength) {
            lock.lock();
            try{
                if(getElapseTimeFromLast() >= unitLength){
                    newCounter += windowPlice[index].intValue();
                    index = ++ index % windowPlice.length;
                    newCounter -= windowPlice[index].intValue();
                    windowPlice[index].getAndSet(0);
                    counter.getAndSet(newCounter);
                    lastTimeStamp = System.currentTimeMillis();
                }
            }catch (Exception e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }
        boolean result = counter.incrementAndGet() <= limit;
        if(result) {
            windowPlice[index].incrementAndGet();
        }
        return result;
    }
}

总结

本文介绍了限流算法中的固定窗口算法和滑动窗口算法,固定窗口算法实现简单,但是会出现2倍配置速率问题,滑动窗口通过将窗口划分成多个细粒度的时间分片,每次移动一个小的时间分片,来解决固定窗口的中的限流失效的问题。对于漏桶算法和令牌桶 算法,放在下一篇文章中讨论。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值