1. 程式人生 > >介面限流看這一篇就夠了!!!

介面限流看這一篇就夠了!!!

## 導讀 - 前幾天和一個朋友討論了他們公司的系統問題,傳統的單體應用,叢集部署,他說近期服務的併發量可能會出現瞬時增加的風險,雖然部署了叢集,但是通過壓測後發現請求延遲仍然是很大,想問問我有什麼改進的地方。我沉思了一會,現在去改架構顯然是不可能的,於是我給出了一個建議,讓他去做個介面限流,這樣能夠保證瞬時併發量飆高也不會出現請求延遲的問題,使用者的體驗度也會上去。 - 至於什麼是介面限流?怎麼實現介面限流?如何實現單機應用的限流?如何實現分散式應用的限流?本篇文章將會詳細闡述。 ## 限流的常見幾種演算法 - 常見的限流演算法有很多,但是最常用的演算法無非以下四種。 ### 固定視窗計數器 ![](https://gitee.com/chenjiabing666/Blog-file/raw/master/8ded7a2b90e1482093f92fff555b3615.png) - 固定演算法的概念如下 1. 將時間劃分為多個視窗 2. 在每個視窗內每有一次請求就將計數器加一 3. 如果計數器超過了限制數量,則本視窗內所有的請求都被丟棄當時間到達下一個視窗時,計數器重置。 - 固定視窗計數器是最為簡單的演算法,但這個演算法有時會讓通過請求量允許為限制的兩倍。考慮如下情況:限制 1 秒內最多通過 5 個請求,在第一個視窗的最後半秒內通過了 5 個請求,第二個視窗的前半秒內又通過了 5 個請求。這樣看來就是在 1 秒內通過了 10 個請求。 ![](https://gitee.com/chenjiabing666/Blog-file/raw/master/4d03e8e43a8edc3f32376d90e52b85f4.png) ### 滑動視窗計數器 ![](https://gitee.com/chenjiabing666/Blog-file/raw/master/ae4d3cd14efb8dc7046d691c90264715.png) - 滑動視窗計數器演算法概念如下: 1. 將時間劃分為多個區間; 2. 在每個區間內每有一次請求就將計數器加一維持一個時間視窗,佔據多個區間; 3. 每經過一個區間的時間,則拋棄最老的一個區間,並納入最新的一個區間; 4. 如果當前視窗內區間的請求計數總和超過了限制數量,則本視窗內所有的請求都被丟棄。 - 滑動視窗計數器是通過將視窗再細分,並且按照時間 " 滑動 ",這種演算法避免了固定視窗計數器帶來的雙倍突發請求,但時間區間的精度越高,演算法所需的空間容量就越大。 ### 漏桶演算法 ![](https://gitee.com/chenjiabing666/Blog-file/raw/master/75938d1010138ce66e38c6ed0392f103.png) - 漏桶演算法概念如下: 1. 將每個請求視作 " 水滴 " 放入 " 漏桶 " 進行儲存; 2. “漏桶 " 以固定速率向外 " 漏 " 出請求來執行如果 " 漏桶 " 空了則停止 " 漏水”; 3. 如果 " 漏桶 " 滿了則多餘的 " 水滴 " 會被直接丟棄。 - 漏桶演算法多使用佇列實現,服務的請求會存到佇列中,服務的提供方則按照固定的速率從佇列中取出請求並執行,過多的請求則放在佇列中排隊或直接拒絕。 - 漏桶演算法的缺陷也很明顯,當短時間內有大量的突發請求時,即便此時伺服器沒有任何負載,每個請求也都得在佇列中等待一段時間才能被響應。 ### 令牌桶演算法 ![](https://gitee.com/chenjiabing666/Blog-file/raw/master/eca0e5eaa35dac938c673fecf2ec9a93.png) - 令牌桶演算法概念如下: 1. 令牌以固定速率生成。 2. 生成的令牌放入令牌桶中存放,如果令牌桶滿了則多餘的令牌會直接丟棄,當請求到達時,會嘗試從令牌桶中取令牌,取到了令牌的請求可以執行。 3. 如果桶空了,那麼嘗試取令牌的請求會被直接丟棄。 - 令牌桶演算法既能夠將所有的請求平均分佈到時間區間內,又能接受伺服器能夠承受範圍內的突發請求,因此是目前使用較為廣泛的一種限流演算法。 ## 單體應用實現 - 在傳統的單體應用中限流只需要考慮到多執行緒即可,使用Google開源工具類guava即可。其中有一個RateLimiter專門實現了單體應用的限流,使用的是令牌桶演算法。 - 單體應用的限流不是本文的重點,官網上現成的API,讀者自己去看看即可,這裡不再詳細解釋。 ## 分散式限流 - 分散式限流和熔斷現在有很多的現成的工具,比如Hystrix,Sentinel 等,但是還是有些企業不引用外來類庫,因此就需要自己實現。 - Redis作為單執行緒多路複用的特性,很顯然能夠勝任這項任務。 ### Redis如何實現 - 使用令牌桶的演算法實現,根據前面的介紹,我們瞭解到令牌桶演算法的基礎需要兩個個變數,分別是桶容量,產生令牌的速率。 - 這裡我們實現的就是每秒產生的速率加上一個桶容量。但是如何實現呢?這裡有幾個問題。 - 需要儲存什麼資料在redis中? - 當前桶的容量,最新的請求時間 - 以什麼資料結構儲存? - 因為是針對介面限流,每個介面的業務邏輯不同,對併發的處理也是不同,因此要細化到每個介面的限流,此時我們選用HashMap的結構,hashKey是介面的唯一id,可以是請求的uri,裡面的分別儲存當前桶的容量和最新的請求時間。 - 如何計算需要放令牌? - 根據redis儲存的上次的請求時間和當前時間比較,如果相差大於的**產生令牌的時間(陳某實現的是1秒)**則再次產生令牌,此時的桶容量為當前令牌+產生的令牌 - 如何保證redis的原子性? - 保證redis的原子性,使用lua指令碼即可解決。 - 有了上述的幾個問題,便能很容易的實現。 ### 開擼 1、lua指令碼如下: ```lua local ratelimit_info = redis.pcall('HMGET',KEYS[1],'last_time','current_token') local last_time = ratelimit_info[1] local current_token = tonumber(ratelimit_info[2]) local max_token = tonumber(ARGV[1]) local token_rate = tonumber(ARGV[2]) local current_time = tonumber(ARGV[3]) if current_token == nil then current_token = max_token last_time = current_time else local past_time = current_time-last_time if past_time>1000 then current_token = current_token+token_rate last_time = current_time end ## 防止溢位 if current_token>max_token then current_token = max_token last_time = current_time end end local result = 0 if(current_token>0) then result = 1 current_token = current_token-1 last_time = current_time end redis.call('HMSET',KEYS[1],'last_time',last_time,'current_token',current_token) return result ``` - 呼叫lua指令碼出四個引數,分別是介面方法唯一id,桶容量,每秒產生令牌的數量,當前請求的時間戳。 2、 SpringBoot程式碼實現 - 採用Spring-data-redis實現lua指令碼的執行。 - Redis序列化配置: ```java /** * 重新注入模板 */ @Bean(value = "redisTemplate") @Primary public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){ RedisTemplate