您好,登錄后才能下訂單哦!
一、限流的作用
由于API接口無法控制調用方的行為,因此當遇到瞬時請求量激增時,會導致接口占用過多服務器資源,使得其他請求響應速度降低或是超時,更有甚者可能導致服務器宕機。
限流(Rate limiting)指對應用服務的請求進行限制,例如某一接口的請求限制為100個每秒,對超過限制的請求則進行快速失敗或丟棄。
限流可以應對:
熱點業務帶來的突發請求;
調用方bug導致的突發請求;
惡意gonji請求。
因此,對于公開的接口最好采取限流措施。
二、為什么要分布式限流
當應用為單點應用時,只要應用進行了限流,那么應用所依賴的各種服務也都得到了保護。
但線上業務出于各種原因考慮,多是分布式系統,單節點的限流僅能保護自身節點,但無法保護應用依賴的各種服務,并且在進行節點擴容、縮容時也無法準確控制整個服務的請求限制。
而如果實現了分布式限流,那么就可以方便地控制整個服務集群的請求限制,且由于整個集群的請求數量得到了限制,因此服務依賴的各種資源也得到了限流的保護。
三、限流的算法
實現限流有很多辦法,在程序中時通常是根據每秒處理的事務數(Transaction per second)來衡量接口的流量。
本文介紹幾種最常用的限流算法:
固定窗口計數器;
滑動窗口計數器;
漏桶;
令牌桶。
1、固定窗口計數器算法
固定窗口計數器算法概念如下:
將時間劃分為多個窗口;
在每個窗口內每有一次請求就將計數器加一;
如果計數器超過了限制數量,則本窗口內所有的請求都被丟棄當時間到達下一個窗口時,計數器重置。
固定窗口計數器是最為簡單的算法,但這個算法有時會讓通過請求量允許為限制的兩倍。考慮如下情況:限制1秒內最多通過5個請求,在第一個窗口的最后半秒內通過了5個請求,第二個窗口的前半秒內又通過了5個請求。這樣看來就是在1秒內通過了10個請求。
2、滑動窗口計數器算法
滑動窗口計數器算法概念如下:
將時間劃分為多個區間;
在每個區間內每有一次請求就將計數器加一維持一個時間窗口,占據多個區間;
每經過一個區間的時間,則拋棄最老的一個區間,并納入最新的一個區間;
如果當前窗口內區間的請求計數總和超過了限制數量,則本窗口內所有的請求都被丟棄。
滑動窗口計數器是通過將窗口再細分,并且按照時間"滑動",這種算法避免了固定窗口計數器帶來的雙倍突發請求,但時間區間的精度越高,算法所需的空間容量就越大。
3、漏桶算法
漏桶算法概念如下:
將每個請求視作"水滴"放入"漏桶"進行存儲;
"漏桶"以固定速率向外"漏"出請求來執行如果"漏桶"空了則停止"漏水";
如果"漏桶"滿了則多余的"水滴"會被直接丟棄。
漏桶算法多使用隊列實現,服務的請求會存到隊列中,服務的提供方則按照固定的速率從隊列中取出請求并執行,過多的請求則放在隊列中排隊或直接拒絕。
漏桶算法的缺陷也很明顯,當短時間內有大量的突發請求時,即便此時服務器沒有任何負載,每個請求也都得在隊列中等待一段時間才能被響應。
4、令牌桶算法
令牌桶算法概念如下:
令牌以固定速率生成;
生成的令牌放入令牌桶中存放,如果令牌桶滿了則多余的令牌會直接丟棄,當請求到達時,會嘗試從令牌桶中取令牌,取到了令牌的請求可以執行;
如果桶空了,那么嘗試取令牌的請求會被直接丟棄。
令牌桶算法既能夠將所有的請求平均分布到時間區間內,又能接受服務器能夠承受范圍內的突發請求,因此是目前使用較為廣泛的一種限流算法。
四、代碼實現
作為如此重要的功能,在Java中自然有很多實現限流的類庫,例如Google的開源項目guava提供了RateLimiter類,實現了單點的令牌桶限流。
而分布式限流常用的則有Hystrix、resilience4j、Sentinel等框架,但這些框架都需引入第三方的類庫,對于國企等一些保守的企業,引入外部類庫都需要經過層層審批,較為麻煩。
分布式限流本質上是一個集群并發問題,而Redis作為一個應用廣泛的中間件,又擁有單進程單線程的特性,天然可以解決分布式集群的并發問題。本文簡單介紹一個通過Redis實現單次請求判斷限流的功能。
1、腳本編寫
經過上面的對比,最適合的限流算法就是令牌桶算法。而為實現限流算法,需要反復調用Redis查詢與計算,一次限流判斷需要多次請求較為耗時。因此我們采用編寫Lua腳本運行的方式,將運算過程放在Redis端,使得對Redis進行一次請求就能完成限流的判斷。
令牌桶算法需要在Redis中存儲桶的大小、當前令牌數量,并且實現每隔一段時間添加新的令牌。最簡單的辦法當然是每隔一段時間請求一次Redis,將存儲的令牌數量遞增。
但實際上我們可以通過對限流兩次請求之間的時間和令牌添加速度來計算得出上次請求之后到本次請求時,令牌桶應添加的令牌數量。因此我們在Redis中只需要存儲上次請求的時間和令牌桶中的令牌數量,而桶的大小和令牌的添加速度可以通過參數傳入實現動態修改。
由于第一次運行腳本時默認令牌桶是滿的,因此可以將數據的過期時間設置為令牌桶恢復到滿所需的時間,及時釋放資源。
編寫完成的Lua腳本如下:
2、執行限流
這里使用Spring Data Redis來進行Redis腳本的調用。
編寫Redis腳本類:
通過RedisTemplate對象執行腳本:
rateLimit方法傳入的key為限流接口的ID,max為令牌桶的最大大小,rate為每秒鐘恢復的令牌數量,返回的boolean即為此次請求是否通過了限流。為了測試Redis腳本限流是否可以正常工作,我們編寫一個單元測試進行測試看看。
設置令牌桶大小為10,令牌桶每秒恢復10個,啟動10個線程在短時間內進行30次請求,并輸出每次限流查詢的結果。日志輸出:
可以看到,在0.1秒內請求的30次請求中,除了初始的10個令牌以及隨時間恢復的1個令牌外,剩下19個沒有取得令牌的請求均返回了false,限流腳本正確的將超過限制的請求給判斷出來了,業務中此時就可以直接返回系統繁忙或接口請求太過頻繁等提示。
3、開發中遇到的問題
1)Lua變量格式
Lua中的String和Number需要通過tonumber()和tostring()進行轉換。
2)Redis入參
Redis的pexpire等命令不支持小數,但Lua的Number類型可以存放小數,因此Number類型傳遞給 Redis時最好通過math.ceil()等方式轉換以避免存在小數導致命令失敗。
3)Time命令
由于Redis在集群下是通過復制腳本及參數到所有節點上,因此無法在具有不確定性的命令后面執行寫入命令,因此只能請求時傳入時間而無法使用Redis的Time命令獲取時間。
3.2版本之后的Redis腳本支持redis.replicate_commands(),可以改為使用Time命令獲取當前時間。
4)潛在的隱患
由于此Lua腳本是通過請求時傳入的時間做計算,因此務必保證分布式節點上獲取的時間同步,如果時間不同步會導致限流無法正常運作。
歡迎工作一到五年的Java工程師朋友們加入我的個人粉絲群Java填坑之路:659655594群內提供免費的Java架構學習資料(里面有高可用、高并發、高性能及分布式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)
合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。