高并发利器之限流

早过忘川 提交于 2020-01-07 18:52:57

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

何为限流?

在开发高并发系统时,有三把利器用来保护系统:缓存、降级和限流。那么何为限流呢?顾名思义,限流就是限制流量,就像你宽带包了1个G的流量,用完了就没了。通过限流,我们可以很好地控制系统的qps,从而达到保护系统的目的。

常见限流算法

1、计算器算法

计数器算法是限流算法里最简单也是最容易实现的一种算法。比如我们规定,对于A接口来说,我们1分钟的访问次数不能超过100个。那么我们可以这么做:在一开 始的时候,我们可以设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果counter的值大于100并且该请求与第一个 请求的间隔时间还在1分钟之内,那么说明请求数过多;如果该请求与第一个请求的间隔时间大于1分钟,且counter的值还在限流范围内,那么就重置 counter。

public class CounterTest {
    public long timeStamp = getNowTime();
    public int reqCount = 0;
    public final int limit = 100; // 时间窗口内最大请求数
    public final long interval = 1000; // 时间窗口ms

    public boolean grant() {
        long now = getNowTime();
        if (now < timeStamp + interval) {
            // 在时间窗口内
            reqCount++;
            // 判断当前时间窗口内是否超过最大请求控制数
            return reqCount <= limit;
        } else {
            timeStamp = now;
            // 超时后重置
            reqCount = 1;
            return true;
        }
    }

    public long getNowTime() {
        return System.currentTimeMillis();
    }
}

这个算法虽然简单,但是有一个十分致命的问题,那就是临界问题(最后一秒发送100个请求)

原因是因为我们统计的精度太低。那么如何很好地处理这个问题呢?或者说,如何将临界问题的影响降低呢?可用采用滑动窗口算法

滑动窗口

滑动窗口,又称rolling window

例如一个时间窗口就是一分钟。然后我们将时间窗口进行划分,比如我们就将滑动窗口 划成了6格,所以每格代表的是10秒钟。每过10秒钟,我们的时间窗口就会往右滑动一格。每一个格子都有自己独立的计数器counter,比如当一个请求 在0:35秒的时候到达,那么0:30~0:39对应的counter就会加1。

计数器算法其实就是滑动窗口算法。只是它没有对时间窗口做进一步地划分,所以只有1格。

由此可见,当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。

2、令牌桶算法

令牌桶算法是比较常见的限流算法之一,大概描述如下:

1)、所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;

2)、根据限流大小,设置按照一定的速率往桶里添加令牌;

3)、桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;

4)、请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;

5)、令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流;

3、漏桶算法

漏桶算法其实很简单,可以粗略的认为就是注水漏水过程,往桶中以一定速率流出水,以任意速率流入水,当水超过桶流量则丢弃,因为桶容量是不变的,保证了整体的速率。

Google Guava限流

Google的Guava工具包中提供了一个限流工具类——RateLimiter,RateLimiter是基于“令牌通算法”来实现限流的。

主要流程如下

主要类关系如下

其中RateLimiter是入口类,它提供了两套工厂方法来创建出两个子类。

例子如下

public class RateLimiterTest {

    public static void main(String[] args) {
        String start = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        RateLimiter limiter = RateLimiter.create(2.0); // 这里的1表示每秒允许处理的量为1个
        for (int i = 1; i <= 10; i++) {
            //limiter.tryAcquire();// 请求RateLimiter, 超过permit直接失败
            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);
    }
}

查看获取令牌源码

  public double acquire(int permits) {
    long microsToWait = reserve(permits);
    stopwatch.sleepMicrosUninterruptibly(microsToWait);
    return 1.0 * microsToWait / SECONDS.toMicros(1L);
  }
  
  final long reserve(int permits) {
      checkPermits(permits);
      synchronized (mutex()) {
        return reserveAndGetWaitLength(permits, stopwatch.readMicros());
      }
    }
    
   final long reserveAndGetWaitLength(int permits, long nowMicros) {
        long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
        return max(momentAvailable - nowMicros, 0);
    }

最终调用reserveEarliestAvailable方法返回下次生成时间,再根据下次生成时间减去当前时间得到需要等待的时间

再看下reserveEarliestAvailable的具体实现在SmoothRateLimiter类内,内容如下

  final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
    resync(nowMicros);
    long returnValue = nextFreeTicketMicros;
    double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
    double freshPermits = requiredPermits - storedPermitsToSpend;
    long waitMicros =
        storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
            + (long) (freshPermits * stableIntervalMicros);

    this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
    this.storedPermits -= storedPermitsToSpend;
    return returnValue;
  }

其中resync方法为同步现在为止需要的产生的令牌,底下根据请求的令牌和当前已产生的令牌计算出还需要产生多少令牌

我们知道令牌通算法是需要产生令牌,但产生令牌的方式有俩种;一种是定时任务进行循环产生,另外一种是可以触发时重新计算。google guava这边采用的第二种。

猜想:这边采用第二种方式是为了提高服务器的性能,避免开启多线性占用资源。

如果实现分布式限流器?

Guava限流可以满足单体式服务限流,但是如今我们很多服务都是分布式,如何实现一个分布式集群呢?上文我们知道限流器的实现原理,现在我们就以redis来实现分布式限流器

新建RedisPerimits类来存放初始化几个初始变量值

/**
 * @author 李俊
 * @Description
 * @Date 2019/9/4 16:26
 */
public class RedisPerimits {
    /**
     * 最大存储令牌数
     */
     Long maxPermits;
    /**
     * 当前存储令牌数
     */
    Long storedPermits;
    /**
     * 添加令牌时间间隔
     */
    Long intervalMillis;
    /**
     * 下次请求可以获取令牌的起始时间,默认当前系统时间
     */
    Long nextFreeTicketMillis = System.currentTimeMillis();

    public RedisPerimits() {

    }
    /**
     *
     * @param maxPermits
     * @param storedPermits
     * @param intervalMillis
     * @param nextFreeTicketMillis
     */
    public RedisPerimits(Long maxPermits, Long storedPermits,
                         Long intervalMillis, Long nextFreeTicketMillis) {
        this.maxPermits = maxPermits;
        this.storedPermits = storedPermits;
        this.intervalMillis = intervalMillis;
        this.nextFreeTicketMillis = nextFreeTicketMillis;
    }

    /**
     * 构建Redis令牌数据模型
     * @param permitsPerSecond 每秒放入的令牌数
     * @param maxBurstSeconds maxPermits由此字段计算,最大存储maxBurstSeconds秒生成的令牌
     * @param nextFreeTicketMillis 下次请求可以获取令牌的起始时间,默认当前系统时间
     */
    public RedisPerimits(Double permitsPerSecond,Integer maxBurstSeconds,
                         Long nextFreeTicketMillis) {
        this( new Double(permitsPerSecond * maxBurstSeconds).longValue(),
                permitsPerSecond.longValue(),
                new Double(TimeUnit.SECONDS.toMillis(1) / permitsPerSecond).longValue(),
                nextFreeTicketMillis);
    }
    /**
     * 计算redis-key过期时长(秒)
     *
     * @return redis-key过期时长(秒)
     */
    public Long expires() {
        Long now = System.currentTimeMillis();
        return 2 * TimeUnit.MINUTES.toSeconds(1) +
                TimeUnit.MILLISECONDS.toSeconds(Math.max(nextFreeTicketMillis, now) - now);
    }


    /**
     * if nextFreeTicket is in the past, reSync to now
     * 若当前时间晚于nextFreeTicketMicros,则计算该段时间内可以生成多少令牌,将生成的令牌加入令牌桶中并更新数据
     *
     * @return 是否更新
     */
    Boolean reSync(Long now) {
        if (now > nextFreeTicketMillis) {
            storedPermits = Math.min(maxPermits,
                    storedPermits + (now - nextFreeTicketMillis) / intervalMillis);
            nextFreeTicketMillis = now;
            return true;
        }
        return false;
    }


    public Long getMaxPermits() {
        return maxPermits;
    }

    public void setMaxPermits(Long maxPermits) {
        this.maxPermits = maxPermits;
    }

    public Long getStoredPermits() {
        return storedPermits;
    }

    public void setStoredPermits(Long storedPermits) {
        this.storedPermits = storedPermits;
    }

    public Long getIntervalMillis() {
        return intervalMillis;
    }

    public void setIntervalMillis(Long intervalMillis) {
        this.intervalMillis = intervalMillis;
    }

    public Long getNextFreeTicketMillis() {
        return nextFreeTicketMillis;
    }

    public void setNextFreeTicketMillis(Long nextFreeTicketMillis) {
        this.nextFreeTicketMillis = nextFreeTicketMillis;
    }
}

新建RedisRateLimiter来实现RateLimiter的相对应功能,内容如下

/**
 * @author 李俊
 * @Description
 * @Date 2019/9/4 16:52
 */
public class RedisRateLimiter {
    /**
     * key Redis key
     */
    private String key;
    /**
     * permitsPerSecond 每秒放入的令牌数
     */
    private Double permitsPerSecond;
    /**
     * maxBurstSeconds 最大存储maxBurstSeconds秒生成的令牌
     */
    private Integer maxBurstSeconds = 60;

    private SyncLock syncLock;



    private StringRedisTemplate stringRedisTemplate;

    public static RedisRateLimiter create(String key, Double permitsPerSecond,
                                          StringRedisTemplate stringRedisTemplate) {
       return new RedisRateLimiter(key,permitsPerSecond,1,stringRedisTemplate);
    }


    public RedisRateLimiter(String key, Double permitsPerSecond,
                            Integer maxBurstSeconds, StringRedisTemplate stringRedisTemplate) {
        this.key = key;
        this.maxBurstSeconds = maxBurstSeconds;
        this.permitsPerSecond = permitsPerSecond;
        syncLock = new SyncLock(key+":RateLimitLock",10L,50L,stringRedisTemplate);
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 获取redis内的值
     * @return
     */
    RedisPerimits getRedisPerimits() {
        RedisPerimits redisPerimits = null;
       String redisPerimitsStr = stringRedisTemplate.opsForValue().get(key+":RateLimit");
       if (StringUtils.isEmpty(redisPerimitsStr)) {
           redisPerimits = new RedisPerimits(permitsPerSecond,maxBurstSeconds,
                   now());
           setRedisPerimits(redisPerimits);
       } else {
           redisPerimits = JSON.parseObject(redisPerimitsStr,RedisPerimits.class);
       }
       return redisPerimits;
    }

    /**
     *
     * @param redisPerimits
     */
    void setRedisPerimits(RedisPerimits redisPerimits){
        String redisPerimitsStr = JSON.toJSONString(redisPerimits);
        stringRedisTemplate.opsForValue()
                .set(key+":RateLimit",redisPerimitsStr,
                        redisPerimits.expires(),TimeUnit.SECONDS);
    }


    Long acquire(Long tokens) {
        Long milliToWait = reserve(tokens);
        try {
            Thread.sleep(milliToWait);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return milliToWait;
    }

    Long acquire() {
        return acquire(1L);
    }

    private Long reserve(Long tokens) {
        checkTokens(tokens);
        try {
            syncLock.lock();
            return reserveAndGetWaitLength(tokens);
        } finally {
            syncLock.unLock();
        }
    }

    /**
     * 尝试获取token
     * @param tokens
     * @param timeout
     * @param unit
     * @return
     */
    Boolean tryAcquire(Long tokens, Long timeout, TimeUnit unit) {
        Long timeoutMicros = Math.max(unit.toMillis(timeout), 0);
        checkTokens(tokens);

        Long milliToWait;
        try {
            syncLock.lock();
            if (!canAcquire(tokens, timeoutMicros)) {
                return false;
            } else {
                milliToWait = reserveAndGetWaitLength(tokens);
            }
        } finally {
            syncLock.unLock();
        }
        try {
            Thread.sleep(milliToWait);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return true;
    }

    private Boolean  canAcquire(Long tokens, Long timeoutMillis){
        return queryEarliestAvailable(tokens) - timeoutMillis <= 0;
    }

    private Long queryEarliestAvailable(Long tokens) {
        Long n = now();
        RedisPerimits redisPerimits = getRedisPerimits();
        redisPerimits.reSync(n);
        Long storedPermitsToSpend = Math.min(tokens, redisPerimits.storedPermits); // 可以消耗的令牌数
        Long freshPermits = tokens - storedPermitsToSpend; // 需要等待的令牌数
        Long waitMillis = freshPermits * redisPerimits.intervalMillis; // 需要等待的时间
        return LongMath.saturatedAdd(redisPerimits.nextFreeTicketMillis - n, waitMillis);
    }

    /**
     *
     * @param tokens
     */
    private void checkTokens(Long tokens) {
        Preconditions.checkArgument(tokens > 0,
                "Requested tokens $tokens must be positive");
    }
    /**
     *
     * @return
     */
    public Boolean tryAcquire() {
      return  tryAcquire(1L, 0L, TimeUnit.MICROSECONDS);
    }


    private Long reserveAndGetWaitLength(Long tokens) {
        Long n = now();
        RedisPerimits redisPerimits = getRedisPerimits();
        redisPerimits.reSync(n);
        Long storedPermitsToSpend = Math.min(tokens, redisPerimits.storedPermits); // 可以消耗的令牌数
        Long freshPermits = tokens - storedPermitsToSpend; // 需要等待的令牌数
        Long waitMillis = freshPermits * redisPerimits.intervalMillis; // 需要等待的时间
        redisPerimits.nextFreeTicketMillis = LongMath.saturatedAdd(redisPerimits.nextFreeTicketMillis, waitMillis);
        redisPerimits.storedPermits -= storedPermitsToSpend;
        setRedisPerimits(redisPerimits);
        return redisPerimits.nextFreeTicketMillis - n;
    }

    public Long now() {
        return System.currentTimeMillis();
    }
}

主要查看reserveAndGetWaitLength的实现方法

其他分布式限流实现

Redis+Lua实现

Nginx+Lua实现

END

欢迎长按下图关注公众号 IT李哥

在这里插入图片描述

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!