【本人禿頂程式設計師】介面限流演算法之漏桶演算法&令牌桶演算法
←←←←←←←←←←←← 快,點關注!
工作中對外提供的API 介面設計都要考慮限流,如果不考慮限流,會成系統的連鎖反應,輕者響應緩慢,重者系統宕機,整個業務線崩潰,如何應對這種情況呢,我們可以對請求進行引流或者直接拒絕等操作,保持系統的可用性和穩定性,防止因流量暴增而導致的系統執行緩慢或宕機。
在開發高併發系統時有三把利器用來保護系統:快取、降級和限流
快取:快取的目的是提升系統訪問速度和增大系統處理容量
降級:降級是當伺服器壓力劇增的情況下,根據當前業務情況及流量對一些服務和頁面有策略的降級,以此釋放伺服器資源以保證核心任務的正常執行
限流:限流的目的是通過對併發訪問/請求進行限速,或者對一個時間視窗內的請求進行限速來保護系統,一旦達到限制速率則可以拒絕服務、排隊或等待、降級等處理
限流演算法
常用的限流演算法有令牌桶和和漏桶,而Google開源專案Guava中的RateLimiter使用的就是令牌桶控制演算法。
漏桶演算法
把請求比作是水,水來了都先放進桶裡,並以限定的速度出水,當水來得過猛而出水不夠快時就會導致水直接溢位,即拒絕服務。
漏斗有一個進水口 和 一個出水口,出水口以一定速率出水,並且有一個最大出水速率:
在漏斗中沒有水的時候:
- 如果進水速率小於等於最大出水速率,那麼,出水速率等於進水速率,此時,不會積水
- 如果進水速率大於最大出水速率,那麼,漏斗以最大速率出水,此時,多餘的水會積在漏斗中
在漏斗中有水的時候:
- 出水口以最大速率出水
- 如果漏斗未滿,且有進水的話,那麼這些水會積在漏斗中
- 如果漏斗已滿,且有進水的話,那麼這些水會溢位到漏斗之外
令牌桶演算法
對於很多應用場景來說,除了要求能夠限制資料的平均傳輸速率外,還要求允許某種程度的突發傳輸。這時候漏桶演算法可能就不合適了,令牌桶演算法更為適合。
令牌桶演算法的原理是系統以恆定的速率產生令牌,然後把令牌放到令牌桶中,令牌桶有一個容量,當令牌桶滿了的時候,再向其中放令牌,那麼多餘的令牌會被丟棄;當想要處理一個請求的時候,需要從令牌桶中取出一個令牌,如果此時令牌桶中沒有令牌,那麼則拒絕該請求。
RateLimiter 用法
新增依賴
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>26.0-jre</version> <!-- or, for Android: --> <version>26.0-android</version> </dependency> public class Test { public static void main(String[] args) { ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(100)); // 指定每秒放1個令牌 RateLimiter limiter = RateLimiter.create(1); for (int i = 1; i < 50; i++) { // 請求RateLimiter, 超過permits會被阻塞 //acquire(int permits)函式主要用於獲取permits個令牌,並計算需要等待多長時間,進而掛起等待,並將該值返回 Double acquire = null; if (i == 1) { acquire = limiter.acquire(1); } elseif (i == 2) { acquire = limiter.acquire(10); } else if (i == 3) { acquire = limiter.acquire(2); } else if (i == 4) { acquire = limiter.acquire(20); } else { acquire = limiter.acquire(2); } executorService.submit(new Task("獲取令牌成功,獲取耗:" + acquire + " 第 " + i + " 個任務執行")); } } } class Task implements Runnable { String str; public Task(String str) { this.str = str; } @Override public void run() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); System.out.println(sdf.format(new Date()) + " | " + Thread.currentThread().getName() + str); } }
響應
2018-08-11 00:26:22.953 | pool-1-thread-1獲取令牌成功,獲取耗:0.0 第 1 個任務執行
2018-08-11 00:26:23.923 | pool-1-thread-2獲取令牌成功,獲取耗:0.98925 第 2 個任務執行
2018-08-11 00:26:33.920 | pool-1-thread-3獲取令牌成功,獲取耗:9.996993 第 3 個任務執行
2018-08-11 00:26:35.920 | pool-1-thread-4獲取令牌成功,獲取耗:1.999051 第 4 個任務執行
2018-08-11 00:26:55.920 | pool-1-thread-5獲取令牌成功,獲取耗:19.999726 第 5 個任務執行
2018-08-11 00:26:57.920 | pool-1-thread-6獲取令牌成功,獲取耗:1.999139 第 6 個任務執行
2018-08-11 00:26:59.920 | pool-1-thread-7獲取令牌成功,獲取耗:1.999806 第 7 個任務執行
2018-08-11 00:27:01.919 | pool-1-thread-8獲取令牌成功,獲取耗:1.999433 第 8 個任務執行
acquire函式主要用於獲取permits個令牌,並計算需要等待多長時間,進而掛起等待,並將該值返回
一個RateLimiter主要定義了發放permits的速率。如果沒有額外的配置,permits將以固定的速度分配,單位是每秒多少permits。預設情況下,Permits將會被穩定的平緩的發放。
預消費能力
從輸出結果可以看出,指定每秒放1個令牌,RateLimiter具有預消費的能力:
acquire1 時,並沒有任何等待 0.0 秒 直接預消費了1個令牌
acquire10時,由於之前預消費了 1 個令牌,故而等待了1秒,之後又預消費了10個令牌
acquire2 時,由於之前預消費了 10 個令牌,故而等待了10秒,之後又預消費了2個令牌
acquire20 時,由於之前預消費了 2 個令牌,故而等待了2秒,之後又預消費了20個令牌
acquire2 時,由於之前預消費了 20 個令牌,故而等待了20秒,之後又預消費了2個令牌
acquire2 時,由於之前預消費了 2 個令牌,故而等待了2秒,之後又預消費了2個令牌
acquire2 時 …
通俗的講「前人挖坑後人跳」,也就說上一次請求獲取的permit數越多,那麼下一次再獲取授權時更待的時候會更長,反之,如果上一次獲取的少,那麼時間向後推移的就少,下一次獲得許可的時間更短。可見,都是有代價的。正所謂:要浪漫就要付出代價。馬上就七夕了,浪漫的代價可能要花錢啊,單身狗們。
令牌桶演算法VS漏桶演算法
漏桶
漏桶的出水速度是恆定的,那麼意味著如果瞬時大流量的話,將有大部分請求被丟棄掉(也就是所謂的溢位)。
令牌桶
生成令牌的速度是恆定的,而請求去拿令牌是沒有速度限制的。這意味,面對瞬時大流量,該演算法可以在短時間內請求拿到大量令牌,而且拿令牌的過程並不是消耗很大的事情。
歡迎大家加入粉絲群:963944895,群內免費分享Spring框架、Mybatis框架SpringBoot框架、SpringMVC框架、SpringCloud微服務、Dubbo框架、Redis快取、RabbitMq訊息、JVM調優、Tomcat容器、MySQL資料庫教學視訊及架構學習思維導圖
最後
不論是對於令牌桶拿不到令牌被拒絕,還是漏桶的水滿了溢位,都是為了保證大部分流量的正常使用,而犧牲掉了少部分流量,這是合理的,如果因為極少部分流量需要保證的話,那麼就可能導致系統達到極限而掛掉,得不償失。
本文講的單機的限流,是JVM級別的的限流,所有的令牌生成都是在記憶體中,在分散式環境下不能直接這麼用,可用使redis限流