高併發下常見的限流算法都在這了!

 限流簡介

如今說到高可用系統,都會說到高可用的保護手段:緩存、降級和限流,本博文就主要說說限流。限流是流量限速(Rate Limit)的簡稱,是指只容許指定的事件進入系統,超過的部分將被拒絕服務、排隊或等待、降級等處理。對於server服務而言,限流爲了保證一部分的請求流量能夠獲得正常的響應,總好過所有的請求都不能獲得響應,甚至致使系統雪崩。限流與熔斷常常被人弄混,博主認爲它們最大的區別在於限流主要在server實現,而熔斷主要在client實現,固然了,一個服務既能夠充當server也能夠充當client,這也是讓限流與熔斷同時存在一個服務中,這兩個概念才容易被混淆。html

那爲何須要限流呢?不少人第一反應就是服務扛不住了因此須要限流。這是不全面的說法,博主認爲限流是由於資源的稀缺或出於安全防範的目的,採起的自我保護的措施。限流能夠保證使用有限的資源提供最大化的服務能力,按照預期流量提供服務,超過的部分將會拒絕服務、排隊或等待、降級等處理。git

如今的系統對限流的支持各有不一樣,可是存在一些標準。在HTTP RFC 6585標準中規定了『429 Too Many Requests 』,429狀態碼錶示用戶在給定時間內發送了太多的請求,須要進行限流(「速率限制」),同時包含一個 Retry-After 響應頭用於告訴客戶端多長時間後能夠再次請求服務.github

HTTP/1.1 429 Too Many Requests
Content-Type: text/html
Retry-After: 3600


  
     <title>Too Many Requests</title>
  
  
     <h1>Too Many Requests</h1>
     <p>I only allow 50 requests per hour to this Web site per
        logged in user.  Try again soon.</p>

不少應用框架一樣集成了,限流功能而且在返回的Header中給出明確的限流標識。golang

  • X-Rate-Limit-Limit:同一個時間段所容許的請求的最大數目;
  • X-Rate-Limit-Remaining:在當前時間段內剩餘的請求的數量;
  • X-Rate-Limit-Reset:爲了獲得最大請求數所等待的秒數。

這是經過響應頭告訴調用方服務端的限流頻次是怎樣的,保證後端的接口訪問上限,客戶端也能夠根據響應的Header調整請求。redis

文章首發於https://chenjiabing666.github.io/2021/08/03/高併發下常見的限流算法都在這了算法

限流分類

限流,拆分來看,就兩個字就是動詞限制,很好理解。可是在不一樣的場景之下就是不一樣資源或指標,多樣性就在中體現。在網絡流量中能夠是字節流,在數據庫中能夠是TPS,在API中能夠是QPS亦能夠是併發請求數,在商品中還能夠是庫存數... ...可是無論是哪種『流』,這個流必須能夠被量化,能夠被度量,能夠被觀察到、能夠統計出來。咱們把限流的分類基於不一樣的方式分爲不一樣的類別,以下圖。數據庫

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

由於篇幅有限,本文只會挑選幾個常見的類型分類進行說明。後端

限流粒度分類

根據限流的粒度分類:緩存

  • 單機限流
  • 分佈式限流

現狀的系統基本上都是分佈式架構,單機的模式已經不多了,這裏說的單機限流更加準確一點的說法是單服務節點限流。單機限流是指請求進入到某一個服務節點後超過了限流閾值,服務節點採起了一種限流保護措施。安全

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

分佈式限流狹義的說法是在接入層實現多節點合併限流,好比NGINX+redis,分佈式網關等,廣義的分佈式限流是多個節點(能夠爲不一樣服務節點)有機整合,造成總體的限流服務。

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

單機限流防止流量壓垮服務節點,缺少對總體流量的感知。分佈式限流適合作細粒度不一樣的限流控制,能夠根據場景不一樣匹配不一樣的限流規則。與單機限流最大的區別,分佈式限流須要中心化存儲,常見的使用redis實現。引入了中心化存儲,就須要解決如下問題:

  • 數據一致性

    在限流模式中理想的模式爲時間點一致性。時間點一致性的定義中要求全部數據組件的數據在任意時刻都是徹底一致的,可是通常來講信息傳播的速度最大是光速,其實並不能達到任意時刻一致,總有必定的時間不一致,對於咱們CAP中的一致性來講只要達到讀取到最新數據便可,達到這種狀況並不須要嚴格的任意時間一致。這隻能是理論當中的一致性模型,能夠在限流中達到線性一致性便可。

  • 時間一致性

    這裏的時間一致性與上述的時間點一致性不同,這裏就是指各個服務節點的時間一致性。一個集羣有3臺機器,可是在某一個A/B機器的時間爲Tue Dec 3 16:29:28 CST 2019,C爲Tue Dec 3 16:29:28 CST 2019,那麼它們的時間就不一致。那麼使用ntpdate進行同步也會存在必定的偏差,對於時間窗口敏感的算法就是偏差點。

  • 超時

    在分佈式系統中就須要網絡進行通訊,會存在網絡抖動問題,或者分佈式限流中間件壓力過大致使響應變慢,甚至是超時時間閾值設置不合理,致使應用服務節點超時了,此時是放行流量仍是拒絕流量?

  • 性能與可靠性

    分佈式限流中間件的資源老是有限的,甚至多是單點的(寫入單點),性能存在上限。若是分佈式限流中間件不可用時候如何退化爲單機限流模式也是一個很好的降級方案。

限流對象類型分類

按照對象類型分類:

  • 基於請求限流
  • 基於資源限流

基於請求限流,通常的實現方式有限制總量限制QPS。限制總量就是限制某個指標的上限,好比搶購某一個商品,放量是10w,那麼最多隻能賣出10w件。微信的搶紅包,羣裏發一個紅包拆分爲10個,那麼最多隻能有10人能夠搶到,第十一我的打開就會顯示『手慢了,紅包派完了』。

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

限制QPS,也是咱們常說的限流方式,只要在接口層級進行,某一個接口只容許1秒只能訪問100次,那麼它的峯值QPS只能爲100。限制QPS的方式最難的點就是如何預估閾值,如何定位閾值,下文中會說到。

基於資源限流是基於服務資源的使用狀況進行限制,須要定位到服務的關鍵資源有哪些,並對其進行限制,如限制TCP鏈接數、線程數、內存使用量等。限制資源更能有效地反映出服務當前地清理,但與限制QPS相似,面臨着如何確認資源的閾值爲多少。這個閾值須要不斷地調優,不停地實踐才能夠獲得一個較爲滿意地值。

限流算法分類

不管是按照什麼維度,基於什麼方式的分類,其限流的底層均是須要算法來實現。下面介紹實現常見的限流算法:

  • 計數器
  • 令牌桶算法
  • 漏桶算法

計數器

固定窗口計數器

計數限流是最爲簡單的限流算法,平常開發中,咱們說的限流不少都是說固定窗口計數限流算法,好比某一個接口或服務1s最多隻能接收1000個請求,那麼咱們就會設置其限流爲1000QPS。該算法的實現思路很是簡單,維護一個固定單位時間內的計數器,若是檢測到單位時間已通過去就重置計數器爲零。

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

其操做步驟:

  1. 時間線劃分爲多個獨立且固定大小窗口;
  2. 落在每個時間窗口內的請求就將計數器加1;
  3. 若是計數器超過了限流閾值,則後續落在該窗口的請求都會被拒絕。但時間達到下一個時間窗口時,計數器會被重置爲0。

下面實現一個簡單的代碼。

package limit

import (
   "sync/atomic"
   "time"
)

type Counter struct {
   Count       uint64   // 初始計數器
   Limit       uint64  // 單位時間窗口最大請求頻次
   Interval    int64   // 單位ms
   RefreshTime int64   // 時間窗口
}

func NewCounter(count, limit uint64, interval, rt int64) *Counter {
   return &amp;Counter{
      Count:       count,
      Limit:       limit,
      Interval:    interval,
      RefreshTime: rt,
   }
}

func (c *Counter) RateLimit() bool {
   now := time.Now().UnixNano() / 1e6
   if now &lt; (c.RefreshTime + c.Interval) {
      atomic.AddUint64(&amp;c.Count, 1)
      return c.Count &lt;= c.Limit
   } else {
      c.RefreshTime = now
      atomic.AddUint64(&amp;c.Count, -c.Count)
      return true
   }
}

測試代碼:

package limit

import (
   "fmt"
   "testing"
   "time"
)

func Test_Counter(t *testing.T) {
   counter := NewCounter(0, 5, 100, time.Now().Unix())
   for i := 0; i &lt; 10; i++ {
      go func(i int) {
         for k := 0; k &lt;= 10; k++ {
            fmt.Println(counter.RateLimit())
            if k%3 == 0 {
               time.Sleep(102 * time.Millisecond)
            }
         }
      }(i)
   }
   time.Sleep(10 * time.Second)
}

看了上面的邏輯,有沒有以爲固定窗口計數器很簡單,對,就是這麼簡單,這就是它的一個優勢實現簡單。同時也存在兩個比較嚴重缺陷。試想一下,固定時間窗口1s限流閾值爲100,可是前100ms,已經請求來了99個,那麼後續的900ms只能經過一個了,就是一個缺陷,基本上沒有應對突發流量的能力。第二個缺陷,在00:00:00這個時間窗口的後500ms,請求經過了100個,在00:00:01這個時間窗口的前500ms還有100個請求經過,對於服務來講至關於1秒內請求量達到了限流閾值的2倍。

滑動窗口計數器

滑動時間窗口算法是對固定時間窗口算法的一種改進,這詞被大衆所知實在TCP的流量控制中。固定窗口計數器能夠說是滑動窗口計數器的一種特例,滑動窗口的操做步驟:

  1. 將單位時間劃分爲多個區間,通常都是均分爲多個小的時間段;
  2. 每個區間內都有一個計數器,有一個請求落在該區間內,則該區間內的計數器就會加一;
  3. 每過一個時間段,時間窗口就會往右滑動一格,拋棄最老的一個區間,並歸入新的一個區間;
  4. 計算整個時間窗口內的請求總數時會累加全部的時間片斷內的計數器,計數總和超過了限制數量,則本窗口內全部的請求都被丟棄。

時間窗口劃分的越細,而且按照時間"滑動",這種算法避免了固定窗口計數器出現的上述兩個問題。缺點是時間區間的精度越高,算法所需的空間容量就越大。

常見的實現方式主要有基於redis zset的方式和循環隊列實現。基於redis zset可將Key爲限流標識ID,Value保持惟一,能夠用UUID生成,Score 也記爲同一時間戳,最好是納秒級的。使用redis提供的 ZADD、EXPIRE、ZCOUNT 和 zremrangebyscore 來實現,並同時注意開啓 Pipeline 來儘量提高性能。實現很簡單,可是缺點就是zset的數據結構會愈來愈大。

漏桶算法

漏桶算法是水先進入到漏桶裏,漏桶再以必定的速率出水,當流入水的數量大於流出水時,多餘的水直接溢出。把水換成請求來看,漏桶至關於服務器隊列,但請求量大於限流閾值時,多出來的請求就會被拒絕服務。漏桶算法使用隊列實現,能夠以固定的速率控制流量的訪問速度,能夠作到流量的「平整化」處理。

你們能夠經過網上最流行的一張圖來理解。

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

漏桶算法實現步驟:

  1. 將每一個請求放入固定大小的隊列進行存儲;
  2. 隊列以固定速率向外流出請求,若是隊列爲空則中止流出;
  3. 如隊列滿了則多餘的請求會被直接拒絕·

漏桶算法有一個明顯的缺陷:當短期內有大量的突發請求時,即便服務器負載不高,每一個請求也都得在隊列中等待一段時間才能被響應。

令牌桶算法

令牌桶算法的原理是系統會以一個恆定的速率往桶裏放入令牌,而若是請求須要被處理,則須要先從桶裏獲取一個令牌,當桶裏沒有令牌可取時,則拒絕服務。從原理上看,令牌桶算法和漏桶算法是相反的,前者爲「進」,後者爲「出」。漏桶算法與令牌桶算法除了「方向」上的不一樣還有一個更加主要的區別:令牌桶算法限制的是平均流入速率(容許突發請求,只要有足夠的令牌,支持一次拿多個令牌),並容許必定程度突發流量;

令牌桶算法的實現步驟:

  1. 令牌以固定速率生成並放入到令牌桶中;
  2. 若是令牌桶滿了則多餘的令牌會直接丟棄,當請求到達時,會嘗試從令牌桶中取令牌,取到了令牌的請求能夠執行;
  3. 若是桶空了,則拒絕該請求。

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

四種策略該如何選擇?

  • 固定窗口:實現簡單,可是過於粗暴,除非狀況緊急,爲了能快速止損眼前的問題能夠做爲臨時應急的方案。
  • 滑動窗口:限流算法簡單易實現,能夠應對有少許突增流量場景。
  • 漏桶:對於流量絕對均勻有很強的要求,資源的利用率上不是極致,但其寬進嚴出模式,保護系統的同時還留有部分餘量,是一個通用性方案。
  • 令牌桶:系統常常有突增流量,並儘量的壓榨服務的性能。

怎麼作限流?

不論使用上述的哪種分類或者實現方式,系統都會面臨一個共同的問題:如何確認限流閾值。有人團隊根據經驗先設定一個小的閾值,後續慢慢進行調整;有的團隊是經過進行壓力測試後總結出來。這種方式的問題在於壓測模型與線上環境不必定一致,接口的單壓不能反饋整個系統的狀態,全鏈路壓測又難以真實反應實際流量場景流量比例。再換一個思路是經過壓測+各應用監控數據。根據系統峯值的QPS與系統資源使用狀況,進行等水位放大預估限流閾值,問題在於系統性能拐點未知,單純的預測不必定準確甚至極大偏離真實場景。正如《Overload Control for Scaling WeChat Microservices》所說,在具備複雜依賴關係的系統中,對特定服務的進行過載控制可能對整個系統有害或者服務的實現有缺陷。但願後續能夠出現一個更加AI的運行反饋自動設置限流閾值的系統,能夠根據當前QPS、資源狀態、RT狀況等多種關聯數據動態地進行過載保護。

不管是哪種方式給出的限流閾值,系統都應該關注如下幾點:

  1. 運行指標狀態,好比當前服務的QPS、機器資源使用狀況、數據庫的鏈接數、線程的併發數等;
  2. 資源間的調用關係,外部鏈路請求、內部服務之間的關聯、服務之間的強弱依賴等;
  3. 控制方式,達到限流後對後續的請求直接拒絕、快速失敗、排隊等待等處理方式

go限流類庫使用

限流的類庫有不少,不一樣語言的有不一樣的類庫,如大Java的有concurrency-limits、Sentinel、Guava 等,這些類庫都有不少的分析和使用方式了,time/rate 也有其精妙的部分,下面開始進入類庫學習階段。

github.com/golang/time/rate

進行源碼分析前的,最應該作的是瞭解類庫的使用方式、使用場景和API。對業務有了初步的瞭解,閱讀代碼就能夠事半功倍。由於篇幅有限後續的博文在對多個限流類庫源碼作分析。

func NewLimiter(r Limit, b int) *Limiter

newLimiter返回一個新的限制器,它容許事件的速率達到r,並容許最多突發b個令牌。也就是說Limter限制時間的發生頻率,但這個桶一開始容量就爲b,而且裝滿b個令牌(令牌池中最多有b個令牌,因此一次最多隻能容許b個事件發生,一個事件花費掉一個令牌),而後每個單位時間間隔(默認1s)往桶裏放入r個令牌。

limter := rate.NewLimiter(10, 5)

上面的例子表示,令牌桶的容量爲5,而且每一秒中就往桶裏放入10個令牌。細心的讀者都會發現函數NewLimiter第一個參數是Limit類型,能夠看源碼就會發現Limit實際上就是float64的別名。

// Limit defines the maximum frequency of some events.
// Limit is represented as number of events per second.
// A zero Limit allows no events.
type Limit float64

限流器還能夠指定往桶裏放入令牌的時間間隔,實現方式以下:

limter := rate.NewLimiter(rate.Every(100*time.Millisecond), 5)

這兩個例子的效果是同樣的,使用第一種方式不會出如今每一秒間隔一會兒放入10個令牌,也是均勻分散在100ms的間隔放入令牌。rate.Limiter提供了三類方法用來限速:

  • Allow/AllowN
  • Wait/WaitN
  • Reserve/ReserveN

下面對比這三類限流方式的使用方式和適用場景。先看第一類方法:

func (lim *Limiter) Allow() bool
func (lim *Limiter) AllowN(now time.Time, n int) bool

Allow 是AllowN(time.Now(), 1)的簡化方法。那麼重點就在方法 AllowN上了,API的解釋有點抽象,說得雲裏霧裏的,能夠看看下面的API文檔解釋:

AllowN reports whether n events may happen at time now. 
Use this method if you intend to drop / skip events that exceed the rate limit. 
Otherwise use Reserve or Wait.

實際上就是爲了說,方法 AllowN在指定的時間時是否能夠從令牌桶中取出N個令牌。也就意味着能夠限定N個事件是否能夠在指定的時間同時發生。這個兩個方法是無阻塞,也就是說一旦不知足,就會跳過,不會等待令牌數量足夠才執行。也就是文檔中的第二行解釋,若是打算丟失或跳過超出速率限制的時間,那麼久請使用該方法。好比使用以前實例化好的限流器,在某一個時刻,服務器同時收到超過了8個請求,若是令牌桶內令牌小於8個,那麼這8個請求就會被丟棄。一個小示例:

func AllowDemo() {
   limter := rate.NewLimiter(rate.Every(200*time.Millisecond), 5)
   i := 0
   for {
      i++
      if limter.Allow() {
         fmt.Println(i, "====Allow======", time.Now())
      } else {
         fmt.Println(i, "====Disallow======", time.Now())
      }
      time.Sleep(80 * time.Millisecond)
      if i == 15 {
         return
      }
   }
}

執行結果:

1 ====Allow====== 2019-12-14 15:54:09.9852178 +0800 CST m=+0.005998001
2 ====Allow====== 2019-12-14 15:54:10.1012231 +0800 CST m=+0.122003301
3 ====Allow====== 2019-12-14 15:54:10.1823056 +0800 CST m=+0.203085801
4 ====Allow====== 2019-12-14 15:54:10.263238 +0800 CST m=+0.284018201
5 ====Allow====== 2019-12-14 15:54:10.344224 +0800 CST m=+0.365004201
6 ====Allow====== 2019-12-14 15:54:10.4242458 +0800 CST m=+0.445026001
7 ====Allow====== 2019-12-14 15:54:10.5043101 +0800 CST m=+0.525090301
8 ====Allow====== 2019-12-14 15:54:10.5852232 +0800 CST m=+0.606003401
9 ====Disallow====== 2019-12-14 15:54:10.6662181 +0800 CST m=+0.686998301
10 ====Disallow====== 2019-12-14 15:54:10.7462189 +0800 CST m=+0.766999101
11 ====Allow====== 2019-12-14 15:54:10.8272182 +0800 CST m=+0.847998401
12 ====Disallow====== 2019-12-14 15:54:10.9072192 +0800 CST m=+0.927999401
13 ====Allow====== 2019-12-14 15:54:10.9872224 +0800 CST m=+1.008002601
14 ====Disallow====== 2019-12-14 15:54:11.0672253 +0800 CST m=+1.088005501
15 ====Disallow====== 2019-12-14 15:54:11.1472946 +0800 CST m=+1.168074801

第二類方法:由於ReserveN比較複雜,第二類先說WaitN。

func (lim *Limiter) Wait(ctx context.Context) (err error)
func (lim *Limiter) WaitN(ctx context.Context, n int) (err error)

相似Wait 是WaitN(ctx, 1)的簡化方法。與AllowN不一樣的是WaitN會阻塞,若是令牌桶內的令牌數不足N個,WaitN會阻塞一段時間,阻塞時間的時長能夠用第一個參數ctx進行設置,把 context 實例爲context.WithDeadline或context.WithTimeout進行制定阻塞的時長。

func WaitNDemo() {
   limter := rate.NewLimiter(10, 5)
   i := 0
   for {
      i++
      ctx, canle := context.WithTimeout(context.Background(), 400*time.Millisecond)
      if i == 6 {
         // 取消執行
         canle()
      }
      err := limter.WaitN(ctx, 4)

      if err != nil {
         fmt.Println(err)
         continue
      }
      fmt.Println(i, ",執行:", time.Now())
      if i == 10 {
         return
      }
   }
}

執行結果:

1 ,執行:2019-12-14 15:45:15.538539 +0800 CST m=+0.011023401
2 ,執行:2019-12-14 15:45:15.8395195 +0800 CST m=+0.312003901
3 ,執行:2019-12-14 15:45:16.2396051 +0800 CST m=+0.712089501
4 ,執行:2019-12-14 15:45:16.6395169 +0800 CST m=+1.112001301
5 ,執行:2019-12-14 15:45:17.0385893 +0800 CST m=+1.511073701
context canceled
7 ,執行:2019-12-14 15:45:17.440514 +0800 CST m=+1.912998401
8 ,執行:2019-12-14 15:45:17.8405152 +0800 CST m=+2.312999601
9 ,執行:2019-12-14 15:45:18.2405402 +0800 CST m=+2.713024601
10 ,執行:2019-12-14 15:45:18.6405179 +0800 CST m=+3.113002301

適用於容許阻塞等待的場景,好比消費消息隊列的消息,能夠限定最大的消費速率,過大了就會被限流避免消費者負載太高。

第三類方法:

func (lim *Limiter) Reserve() *Reservation
func (lim *Limiter) ReserveN(now time.Time, n int) *Reservation

與以前的兩類方法不一樣的是Reserve/ReserveN返回了Reservation實例。Reservation在API文檔中有5個方法:

func (r *Reservation) Cancel() // 至關於CancelAt(time.Now())
func (r *Reservation) CancelAt(now time.Time)
func (r *Reservation) Delay() time.Duration // 至關於DelayFrom(time.Now())
func (r *Reservation) DelayFrom(now time.Time) time.Duration
func (r *Reservation) OK() bool

經過這5個方法可讓開發者根據業務場景進行操做,相比前兩類的自動化,這樣的操做顯得複雜多了。經過一個小示例來學習Reserve/ReserveN:

func ReserveNDemo() {
   limter := rate.NewLimiter(10, 5)
   i := 0
   for {
      i++
      reserve := limter.ReserveN(time.Now(), 4)
      // 若是爲flase說明拿不到指定數量的令牌,好比須要的令牌數大於令牌桶容量的場景
      if !reserve.OK() {
         return
      }
      ts := reserve.Delay()
      time.Sleep(ts)
      fmt.Println("執行:", time.Now(),ts)
      if i == 10 {
         return
      }
   }
}

執行結果:

執行:2019-12-14 16:22:26.6446468 +0800 CST m=+0.008000201 0s
執行:2019-12-14 16:22:26.9466454 +0800 CST m=+0.309998801 247.999299ms
執行:2019-12-14 16:22:27.3446473 +0800 CST m=+0.708000701 398.001399ms
執行:2019-12-14 16:22:27.7456488 +0800 CST m=+1.109002201 399.999499ms
執行:2019-12-14 16:22:28.1456465 +0800 CST m=+1.508999901 398.997999ms
執行:2019-12-14 16:22:28.5456457 +0800 CST m=+1.908999101 399.0003ms
執行:2019-12-14 16:22:28.9446482 +0800 CST m=+2.308001601 399.001099ms
執行:2019-12-14 16:22:29.3446524 +0800 CST m=+2.708005801 399.998599ms
執行:2019-12-14 16:22:29.7446514 +0800 CST m=+3.108004801 399.9944ms
執行:2019-12-14 16:22:30.1446475 +0800 CST m=+3.508000901 399.9954ms

若是在執行Delay()以前操做Cancel()那麼返回的時間間隔就會爲0,意味着能夠當即執行操做,不進行限流。

func ReserveNDemo2() {
   limter := rate.NewLimiter(5, 5)
   i := 0
   for {
      i++
      reserve := limter.ReserveN(time.Now(), 4)
      // 若是爲flase說明拿不到指定數量的令牌,好比須要的令牌數大於令牌桶容量的場景
      if !reserve.OK() {
         return
      }

      if i == 6 || i == 5 {
         reserve.Cancel()
      }
      ts := reserve.Delay()
      time.Sleep(ts)
      fmt.Println(i, "執行:", time.Now(), ts)
      if i == 10 {
         return
      }
   }
}

執行結果:

1 執行:2019-12-14 16:25:45.7974857 +0800 CST m=+0.007005901 0s
2 執行:2019-12-14 16:25:46.3985135 +0800 CST m=+0.608033701 552.0048ms
3 執行:2019-12-14 16:25:47.1984796 +0800 CST m=+1.407999801 798.9722ms
4 執行:2019-12-14 16:25:47.9975269 +0800 CST m=+2.207047101 799.0061ms
5 執行:2019-12-14 16:25:48.7994803 +0800 CST m=+3.009000501 799.9588ms
6 執行:2019-12-14 16:25:48.7994803 +0800 CST m=+3.009000501 0s
7 執行:2019-12-14 16:25:48.7994803 +0800 CST m=+3.009000501 0s
8 執行:2019-12-14 16:25:49.5984782 +0800 CST m=+3.807998401 798.0054ms
9 執行:2019-12-14 16:25:50.3984779 +0800 CST m=+4.607998101 799.0075ms
10 執行:2019-12-14 16:25:51.1995131 +0800 CST m=+5.409033301 799.0078ms

看到這裏time/rate的限流方式已經完成,除了上述的三類限流方式,time/rate還提供了動態調整限流器參數的功能。相關API以下:

func (lim *Limiter) SetBurst(newBurst int) // 至關於SetBurstAt(time.Now(), newBurst).
func (lim *Limiter) SetBurstAt(now time.Time, newBurst int)// 重設令牌桶的容量
func (lim *Limiter) SetLimit(newLimit Limit) // 至關於SetLimitAt(time.Now(), newLimit)
func (lim *Limiter) SetLimitAt(now time.Time, newLimit Limit)// 重設放入令牌的速率

這四個方法可讓程序根據自身的狀態動態的調整令牌桶速率和令牌桶容量。

結尾

經過上述一系列講解,相信你們對各個限流的應用場景和優缺點也有了大體的掌握,但願在平常開發中有所幫助。限流僅僅是整個服務治理中的一個小環節,須要與多種技術結合使用,才能夠更好的提高服務的穩定性的同時提升用戶體驗。

 

相關文章
相關標籤/搜索