限流原理和代碼實現

介紹

面對高併發的三大利器:限流、降級和緩存。這裏就來談談限流。

爲什麼需要限流?當服務接口的QPS或者併發量超過了接口的承受能力時,可能會導致接口處理變慢,更爲嚴重的,會導致應用崩潰。因此,需要對接口進行限流保護,不單單保護接口自身的應用,也保護接口所依賴的第三方資源。

限流的本質是對突發流量進行整形,從而實現保護系統的目的。

分類

按照限流類型來說,可分爲併發數限流和QPS限流。

併發數限流:限制同一時刻的最大併發請求數量;

QPS限流:限制一段時間內的的請求數量,也就是說在一定的時間窗口內進行限制。如固定窗口限流、滑動窗口限流、漏桶和令牌桶。

按照作用的範圍來說,可分爲單機限流和分佈式限流。

單機限流:針對單臺機器進行限流;

分佈式限流:針對集羣進行限流,需要通過公共服務或者中間件(redis)來存儲數據;

原理和代碼實現

併發數限流

定義:限制同一時刻的併發數。缺點:只能在一個時間點對併發數進行限制,無法做到在時間段內的限流。這裏分別採用原子類和Semaphore實現。

  • 原子類實現的核心代碼如下:

    public class AtomicCountRateLimiter extends AbstractCountRateLimiter{
    
        /**
         * 允許的最大併發數
         */
        private Long maxPermits;
    
        /**
         * 計數器
         */
        private AtomicLong count = new AtomicLong();
    
        public AtomicCountRateLimiter(long maxPermits) {
            this.maxPermits = maxPermits;
        }
    
        /**
         * 獲取許可
         * @return true:正常,不會被限流
         */
        @Override
        public boolean tryAcquire() {
            if(count.incrementAndGet() > maxPermits) {
                return false;
            } else {
                return true;
            }
        }
    
        /**
         * 釋放許可
         */
        @Override
        public void release() {
            count.decrementAndGet();
        }
    }
    
  • Semaphore實現的核心代碼如下:

    public class SemaphoreCountRateLimiter extends AbstractCountRateLimiter{
    
        /**
         * 允許的最大併發數
         */
        private Integer maxPermits;
    
        private Semaphore semaphore;
    
        public SemaphoreCountRateLimiter(int maxPermits) {
            this.maxPermits = maxPermits;
            semaphore = new Semaphore(maxPermits);
        }
    
    
        @Override
        public boolean tryAcquire() {
            return semaphore.tryAcquire();
        }
    
        @Override
        public void release() {
            semaphore.release();
        }
    }
    

固定時間窗口限流

併發數限流存在 無法針對時間段內的請求數(如QPS) 進行限制的缺點,因此可以通過固定窗口完成時間段內的限流。

  • 代碼實現如下:

    維護原子變量reqCount表示當前請求數,時間戳lastVisitedTime表示上一次請求的時間。當來了新的請求後,判斷當前時間戳和lastVisitedTime是否相差大於1s,是則重置reqCount並更新lastVisitedTime爲當前的時間戳,否則直接進行請求數+1;

    public class WindowsRateLimiter implements RateLimiter{
    
        private AtomicLong reqCount;
    
        private Long maxPermits;
    
        private long lastVisitedTime = System.currentTimeMillis();
    
        public WindowsRateLimiter(Long maxPermits) {
            this.maxPermits = maxPermits;
            reqCount = new AtomicLong();
        }
    
        /**
         * 是否獲得許可, 即不會被限流
         * @return true 通過
         */
        @Override
        public synchronized boolean tryAcquire() {
            long now = System.currentTimeMillis();
            if(now - lastVisitedTime > 1000) {
                reqCount.getAndSet(0);
                lastVisitedTime = now;
            }
            reqCount.incrementAndGet();
            return reqCount.longValue() <= maxPermits;
        }
    }
    
  • 缺點:固定窗口無法解決相鄰時間的邊界問題。比如限制最大的QPS爲100,第一秒的後500ms來了100個請求,第二秒的前500ms來了100個請求,那麼由第一秒的後500ms和第二秒的前500ms組成的1s內,最大的QPS已經爲200了,但是沒有進行限制。基於這個問題,引出了滑動時間窗口算法。

滑動時間窗口限流

在這裏插入圖片描述

  • 基本思想:將1s進行拆分爲10個時間段組成,每個時間段是100ms。當收到一個請求時,會在對應的時間段內對請求進行累加。維護一個定時任務,每隔100ms移動當前窗口的位置,並從總的計數中減去窗口中的計數。

  • 解決了相鄰時間的邊界問題,因爲並不是直接把當前請求數直接清0,而是淘汰掉最早的100ms內的計數,最終始終維護了1s內的時間窗口的計數。

  • 問題

    • 需要爲每個窗口維護一個定時任務,浪費資源
    • 窗口劃分的問題,如果劃分的太粗,依然會有邊界問題,劃分太細,定時任務需要按照毫秒或者微妙執行,資源消耗高,同時精度也越高
  • 核心代碼實現如下:

    public class SlidingWindowRateLimiter implements RateLimiter {
    
        /**
         * 允許的最大請求數
         */
        private Long maxPermits;
    
        /**
         * 滑動窗口數組
         */
        private AtomicLong[] slidingWindows;
    
        /**
         * 窗口大小
         */
        private int windowSize;
    
        /**
         * 當前的請求數量
         */
        private AtomicLong reqCount = new AtomicLong();
    
        /**
         * 當前處於滑動窗口的位置
         */
        private volatile int index;
    
        private ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(1);
    
        public SlidingWindowRateLimiter(long maxPermits, int windowSize) {
            this.maxPermits = maxPermits;
            this.windowSize = windowSize;
            slidingWindows = new AtomicLong[windowSize];
            for (int i = 0; i < windowSize; i++) {
                slidingWindows[i] = new AtomicLong();
            }
            index = 0;
            init();
        }
    
        /**
         * 獲得許可
         *
         * @return true: 正常通過, false: 被限流
         */
        @Override
        public boolean tryAcquire() {
            slidingWindows[index].incrementAndGet();
            reqCount.incrementAndGet();
            return !isOverLimit();
        }
    
        /**
         * 是否被限流
         *
         * @return true:被限流
         */
        private boolean isOverLimit() {
            return reqCount.get() > maxPermits;
        }
    
        /**
         * 定時移動窗口位置, reqCount並不是直接清零, 所以不會有邊界問題
         */
        private void init() {
            if(1000 % windowSize != 0) {
                throw new IllegalArgumentException("windowSize必須能夠被1000整除");
            }
            long period = 1000 / windowSize;
            scheduledExecutor.scheduleAtFixedRate(new Runnable() {
                @Override
                public void run() {
                    index = (index + 1) % windowSize;
                    long val = slidingWindows[index].getAndSet(0);
                    reqCount.addAndGet(-val);
                }
            }, 0, period, TimeUnit.MILLISECONDS);
        }
    }
    

漏桶

待續

令牌桶

待續

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章