在高併發的分佈式系統,如大型電商系統中,因爲接口 API 沒法控制上游調用方的行爲,所以當瞬間請求量突增時,會致使服務器佔用過多資源,發生響應速度下降、超時乃至宕機,甚至引起雪崩形成整個系統不可用。java
面對這種狀況,一方面咱們會提高 API 的吞吐量和 QPS(Query Per Second 每秒查詢量),但總歸會有上限,因此另外一方面爲了應對巨大流量的瞬間提交,咱們須要作對應的限流處理,也就是對請求量進行限制,對於超出限制部分的請求做出快速拒絕、快速失敗、丟棄處理,以保證本服務以及下游資源系統的穩定。redis
常見的限流算法有計數器、漏斗、令牌桶。算法
計數器限流方式比較粗暴,一次訪問就增長一次計數,在系統內設置每 N 秒的訪問量,超過訪問量的訪問直接丟棄,從而實現限流訪問。具體大概是如下步驟:數組
這種算法的弊端是,在開始的時間,訪問量被使用完後,1 s 內會有很長時間的真空期是處於接口不可用的狀態的,同時也有可能在一秒內出現兩倍的訪問量。服務器
實現方式和擴展方式不少,這裏以 Redis 舉例簡單的實現,計數器主要思路就是在單位時間內,有且僅有 N 數量的請求可以訪問個人代碼程序。因此能夠利用 Redis 的 setnx
來實現這方面的功能。網絡
好比如今須要在 10 秒內限定 20 個請求,那麼能夠在 setnx
的時候設置過時時間 10,當請求的 setnx
數量達到 20 的時候即達到了限流效果。數據結構
滑動窗口計數法的思路是:併發
利用 Redis 的 list 數據結構能夠垂手可得地實現該功能。咱們能夠將請求打形成一個 zset 數組,當每一次請求進來的時候,key 保持惟一,value 能夠用 UUID 生成,而 score 能夠用當前時間戳表示,由於 score 咱們能夠用來計算當前時間戳以內有多少的請求數量。而 zset 數據結構也提供了 range 方法讓咱們能夠很輕易地獲取到兩個時間戳內有多少請求。負載均衡
public Response limitFlow() { Long currentTime = new Date().getTime(); if (redisTemplate.hasKey("limit")) { Integer count = redisTemplate.opsForZset().rangeByScore("limit", currentTime - intervalTime, currentTime).size(); if (count != null && count > 5) { return Response.ok("每分鐘最多隻能訪問 5 次!"); } } redisTemplate.opsForZSet().add("limit", UUID.randomUUID().toString(), currentTime); return Response.ok("訪問成功"); }
經過上述代碼能夠作到滑動窗口的效果,而且能保證每 N 秒內至多 M 個請求,實現方式相對來講也是比較簡單的,可是所帶來的缺點就是 zset 的數據結構會愈來愈大。dom
在計數器算法中咱們看到,當使用了全部的訪問量後,接口會徹底處於不可用狀態,有些系統不能接受這樣的處理方式,對此可使用漏斗算法進行限流,漏斗算法的原理就像名字,訪問量從漏斗的大口進入,從漏斗的小口進入系統。這樣不論是多大的訪問量進入漏斗,最後進入系統的訪問量都是固定的。漏斗的好處就是,大批量訪問進入時,漏斗有容量,不超過容量(容量的設計=固定處理的訪問量 * 可接受等待時長)的數據均可以排隊等待處理,超過的纔會丟棄。
實現方式可使用隊列,隊列設置容量,訪問能夠大批量塞入隊列,滿隊列後丟棄後續訪問量。隊列的出口以固定速率拿去訪問量處理。
這種方案因爲出口速率是固定的,因此並無辦法應對短期的突發流量。
令牌桶算法是漏斗算法的改進版,爲了處理短期的突發流量而作了優化,令牌桶算法主要由三部分組成:令牌流
、數據流
、令牌桶
。
名詞釋義:
令牌桶會按照必定的速率生成令牌放入令牌桶,訪問要進入系統時,須要從令牌桶中獲取令牌,有令牌的能夠進入,沒有的被拋棄,因爲令牌桶的令牌是源源不斷生成的,當訪問量小時,能夠留存令牌達到令牌桶的上限,這樣當短期的突發訪問量時,積累的令牌數能夠處理這個問題。當訪問量持續大量流入時,因爲生成令牌的速率是固定的,最後也就變成了相似漏斗算法的固定流量處理。
實現方式和漏斗也比較相似,可使用一個隊列保存令牌,一個定時任務用等速率生成令牌放入隊列,訪問量進入系統時,從隊列獲取令牌再進入系統。
google
開源的 guava
包中的 RateLimiter
類實現了令牌桶算法,不一樣其實現方式是單機的,集羣能夠按照上面的實現方式,隊列使用中間件 MQ 實現,配合負載均衡算法,考慮集羣各個服務器的承壓狀況作對應服務器的隊列是較好的作法。
這裏簡單用 Redis 以及定時任務模擬大概的過程:
首先依靠 List 的 leftPop 來獲取令牌:
// 輸出令牌 public Response limitFlow() { Object result = redisTemplate.opsForList().leftPop("limit_list"); if (result == null) { return Response.ok("當前令牌桶中無令牌!"); } return Response.ok("訪問成功!"); }
再依靠 Java 的定時任務,定時往 List 中 rightPush 令牌,固然令牌也須要保證惟一性,因此這裏利用 UUID 生成:
// 10S的速率往令牌桶中添加UUID,只爲保證惟一性 @Scheduled(fixedDelay = 10_000,initialDelay = 0) public void setIntervalTimeTask(){ redisTemplate.opsForList().rightPush("limit_list",UUID.randomUUID().toString()); }
單點應用下,對應用進行限流,既能知足本服務的需求,又能夠很好地保護好下游資源。在選型上,能夠採用上面說起的 Google Guava 的 RateLimiter。
而在多機部署的場景下,對單點的限流,並不能達到咱們想要的最好效果,須要引入分佈式限流。分佈式限流的算法,依然能夠採用令牌桶算法,只不過將令牌桶的發放、存儲改成全局的模式。
在真實應用場景,能夠採用 redis + lua 的方式,經過把邏輯放在 redis 端,來減小調用次數。
lua 的邏輯以下:
文章內容收集於網絡。