為什么面試官喜歡考我限流的知識
本文公眾號來源:安琪拉的博客作者:安琪拉本文已收錄至我的GitHub
在系統(tǒng)設(shè)計(jì)中,限流是保障系統(tǒng)高可用的一種常規(guī)手段,同樣的手段還有熔斷、服務(wù)降級等等。
妲己:聽說最近你們系統(tǒng)又對接了幾條業(yè)務(wù)線,而且早上9.10點(diǎn)鐘流量非常大,你怎么保證系統(tǒng)不被搞掛?
安琪拉:你算是問對了,最近對接了幾家大機(jī)構(gòu),同時(shí)由于疫情的影響,線上渠道的流量也比平常多了很多,我這邊系統(tǒng)做了很多系統(tǒng)優(yōu)化,大致可以歸為以下幾類:
- 限流:對應(yīng)用入口流量做控制,瞬時(shí)流量向后遷移,對下游請求流量做自適應(yīng)限流,根據(jù)接口響應(yīng)時(shí)間動(dòng)態(tài)調(diào)整流量。
- 延遲排隊(duì):如果請求量大,按業(yè)務(wù)線優(yōu)先級排隊(duì),優(yōu)先保障線上渠道實(shí)時(shí)的請求。
- 路由:這個(gè)是因?yàn)闃I(yè)務(wù)的特殊性,所有的請求都依賴下游第三方的服務(wù),可以將多家下游服務(wù)供應(yīng)商做個(gè)動(dòng)態(tài)路由表,將請求優(yōu)先路由給接口成功率高、耗時(shí)低的服務(wù)供應(yīng)商;
- 備份:這基本是所有分布式組件都會做的,能做多機(jī)的不做單機(jī),例如:Redis 做三主三備(集群)、MySQL分庫分表、MQ 與 Redis 互為備份(安琪拉遇到過MQ事故)等等;
- 降級:這個(gè)是最后的迫不得已的措施,如果遇到全線崩潰,使用降級手段保障系統(tǒng)核心功能可用,或讓模塊達(dá)到最小可用。
- 日志:完整的監(jiān)控和鏈路日志,日志功能很多,也分很多種,一方面是方便排查問題,另一方面可用來做任務(wù)重試、任務(wù)回滾、數(shù)據(jù)恢復(fù)、狀態(tài)持久化等。
妲己:能給我講講限流的基礎(chǔ)概念嗎?
安琪拉:限流,顧名思義,就是限制流量,一般分為限制入口流量和限制出口流量,入口流量是人家來請求我的系統(tǒng),我在入口處加了一道閥門,出口流量是我調(diào)外部系統(tǒng),我在出口加一道閥門。
妲己:那一般怎么來實(shí)現(xiàn)限流呢?
安琪拉:如果是單機(jī),可以通過Semphore 限制統(tǒng)一時(shí)間請求接口的量,也可以用 Google Guava 包提供的限流包,如果是分布式環(huán)境,可以使用 Redis 實(shí)現(xiàn),也有阿里 Sentinal 或 Spring Cloud Gateway 可以實(shí)現(xiàn)限流。我們先來看單機(jī)版本的限流,代碼如下:

這個(gè)代碼很好理解,一共就只有三個(gè)坑位,使用 Semaphore 定義,"妲己", "亞瑟", "魯班", "甄姬" 輪番上總共二十次廁所,Semaphore 就是鎖的機(jī)制,進(jìn)了廁所,在門上加鎖,看下控制臺輸出:

每次只有三個(gè)人能同時(shí)上廁所。
妲己:我似乎有點(diǎn)明白了,廁所就是資源,我們上廁所就好比請求,大家一起上就產(chǎn)生了流量高峰,那分布式環(huán)境怎么解決呢?
安琪拉:分布式環(huán)境的思想和單機(jī)的思想是一樣的,也是控制資源的訪問頻率,一般主流的設(shè)計(jì)思想有二種:
漏洞算法

把請求比作水,在請求入口和響應(yīng)請求的服務(wù)之間加一個(gè)漏桶,桶中的水以恒定的速度流出,這樣保證了服務(wù)接收到的流量速度是穩(wěn)定的,如果桶里的水滿了,再進(jìn)來的水就直接溢出(請求直接拒絕)。
漏桶是網(wǎng)絡(luò)環(huán)境中流量整形(Traffic Shaping)或速率限制(Rate Limiting)時(shí)經(jīng)常使用的一種算法,它的主要目的是控制數(shù)據(jù)進(jìn)入到網(wǎng)絡(luò)的速率,平滑網(wǎng)絡(luò)上的突發(fā)流量。
令牌桶算法

令牌桶算法有點(diǎn)類似于生產(chǎn)者消費(fèi)者模式,專門有一個(gè)生產(chǎn)者往令牌桶中以恒定速率放入令牌,而請求處理器(消費(fèi)者)在處理請求時(shí)必須先從桶中獲得令牌,如果沒有拿到令牌,有二種策略:一種是直接返回拒絕請求,一種是等待一段時(shí)間,再次嘗試獲取令牌。
令牌桶算法用來控制發(fā)送到網(wǎng)絡(luò)上的數(shù)據(jù)的數(shù)目,并允許突發(fā)數(shù)據(jù)的發(fā)送
Google的Guava包中的RateLimiter類就是令牌桶算法的解決方案。
對比一下這二種算法,其實(shí)無非是一個(gè)在出口處是以恒定的速率出水,一個(gè)是以恒定速率放令牌,安琪拉看來區(qū)分度不大,只是令牌桶算法更加靈活,往往實(shí)際工作中,可以實(shí)現(xiàn)動(dòng)態(tài)調(diào)整令牌的放入速度、以及令牌桶的總大小。
妲己:為什么我看完覺得二種算法差不多?
安琪拉:令牌桶相比漏桶有個(gè)優(yōu)勢,能夠滿足突發(fā)流量的請求。打個(gè)比方:如果線上環(huán)境資源很空閑,因?yàn)槁┒此鞒龅乃俣群愣?,請求因?yàn)樗俣仁芟薏粫皶r(shí)得到響應(yīng)。比如現(xiàn)在漏洞出水速度是 3個(gè)/秒,現(xiàn)在線上來了5個(gè)請求,全部進(jìn)漏桶,漏桶里面現(xiàn)在一共只有5個(gè)請求,但是也只能一秒處理 3 個(gè)(出水速度限制)。但是如果是令牌桶算法,放入令牌的速度是 3個(gè)/秒,假設(shè)令牌桶中已經(jīng)有二個(gè)令牌了,這時(shí)來了5個(gè)請求,都能拿到令牌完成請求,因此令牌桶算法是面向請求的(請求主動(dòng)拿令牌,按需分配),而漏洞則是面向令牌,我以恒定的速度出水,才不管你有多少請求。
妲己:我明白了,那你給我講講 Google Guava怎么實(shí)現(xiàn)令牌桶算法的?
安琪拉:明白了思想之后,很容易理解實(shí)現(xiàn),我們來看一下源代碼:

API 很簡單,只需要指定限流的速度,例如第一個(gè), 速度是每秒鐘2個(gè),如果是分鐘級限流,你也可以設(shè)置為 0.2,代表1秒鐘生成0.2 個(gè)令牌,1分鐘限流為 12個(gè)。第二個(gè)例子是每秒鐘5000,這個(gè)例子演示了如何通過限流器限制網(wǎng)絡(luò)處理流量為每秒鐘 5kb。5000個(gè)byte。
Guava 還有很多方法,如下:
| 返回值和方法修飾符 | 方法和描述 |
|---|---|
| double | acquire() 從RateLimiter獲取一個(gè)許可,方法會被阻塞直到獲取到請求 |
| double | acquire(int permits) 從RateLimiter獲取指定許可數(shù),方法會被阻塞直到獲取到請求 |
| static RateLimiter | create(double permitsPerSecond) 根據(jù)每秒放到令牌桶數(shù)量創(chuàng)建RateLimiter,這里的令牌桶數(shù)量是指每秒生成令牌數(shù)(通常是指QPS,每秒多少查詢) |
| static RateLimiter | create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) 根據(jù)每秒放到令牌桶數(shù)量和預(yù)熱期來創(chuàng)建RateLimiter,意思是不會一下生成全部的令牌,把令牌桶塞滿,而是會漸進(jìn)式的增加令牌,這里的每秒放到令牌桶數(shù)量是指每秒生成令牌數(shù)(通常是指QPS,每秒多少個(gè)請求量),在這段預(yù)熱時(shí)間內(nèi),RateLimiter每秒分配的許可數(shù)會平穩(wěn)地增長直到預(yù)熱期結(jié)束時(shí)達(dá)到最大。 |
| double | getRate() 返回RateLimiter 配置中的穩(wěn)定速率,該速率單位是每秒生成多少令牌數(shù) |
| void | setRate(double permitsPerSecond) 更新RateLimite的穩(wěn)定速率,參數(shù)permitsPerSecond 由構(gòu)造RateLimiter的工廠方法提供。 |
| boolean | tryAcquire() 從RateLimiter 獲取許可,如果該許可可以在無延遲下的情況下立即獲取得到的話 |
| boolean | tryAcquire(int permits) 從RateLimiter 獲取許可數(shù),如果該許可數(shù)可以在無延遲下的情況下立即獲取得到的話 |
| boolean | tryAcquire(int permits, long timeout, TimeUnit unit) 從RateLimiter 獲取指定許可數(shù)如果該許可數(shù)可以在不超過timeout的時(shí)間內(nèi)獲取得到的話,或者如果無法在timeout 過期之前獲取得到許可數(shù)的話,那么立即返回false (無需等待) |
妲己:那我們來看一下分布式限流吧。
安琪拉:分布式其實(shí)就是把本地令牌桶放到一個(gè)所有主機(jī)都可以訪問的地方。
妲己:一般放哪里比較合適。
安琪拉:分布式中間件,例如 Redis,分布式緩存,生成令牌和獲取令牌都可以用redis指令實(shí)現(xiàn),而且速度還快。
妲己:那你快給我講講怎么實(shí)現(xiàn)。
安琪拉:那我說下我的實(shí)現(xiàn)步驟和背景吧。在令牌桶算法中,有一個(gè)單獨(dú)的生產(chǎn)者以恒定的速率向令牌桶中放入令牌,如果通過redis實(shí)現(xiàn),一個(gè)生產(chǎn)者線程不斷往redis添加令牌(寫),其他請求線程每次請求讀redis獲取令牌,這樣會有很大的性能損耗,好的解決辦法是延遲放令牌的操作,獲取令牌的時(shí)候才放入令牌,將二個(gè)操作合并。
妲己:那獲取令牌的時(shí)候怎么計(jì)算應(yīng)該放桶中放入多少令牌呢?
安琪拉:嗯,這是個(gè)好問題,filledTokens 為這一次需要放入令牌的數(shù)量,計(jì)算邏輯為:
此刻應(yīng)填充令牌數(shù) = min((令牌桶剩余令牌數(shù) + 當(dāng)前時(shí)間與上一次令牌生成時(shí)間間隔 * 令牌生成速度), 令牌總?cè)萘?
根據(jù)上面的思路,寫一個(gè)分布式限流的Redis腳本,redis提供lua支持,腳本如下:

妲己:這個(gè)寫好之后怎么測試 ?
安琪拉:關(guān)注Wx 公眾號:安琪拉的博客, 我教你!Redis 提供了客戶端加載工具可以方便lua 腳本的調(diào)試,如下所示:

如下圖所示:可以輸入help 查看完整命令,常用n和print,分別為下一行和打印當(dāng)前局部變量
[(img-KHHPnrUg-1586806007625)(/Users/zw/Library/Application Support/typora-user-images/image-20200414025001967.png)]
另外也可以直接通過script load 命令加載redis lua腳本,得到sha1 之后直接運(yùn)行(這個(gè)是模型真實(shí)程序運(yùn)行模式,可以暫時(shí)跳過)。
// 1. 在redis服務(wù)端load 腳本 拿到sharedis-cli script load "$(cat ratelimit.lua)"//sha1: ebbcd2ed99990afaca6d2ba61a0f2d5bdd907e59// 2. 通過腳本 sha1 值運(yùn)行腳本redis-cli evalsha ebbcd2ed99990afaca6d2ba61a0f2d5bdd907e59 2 remain.${0}.tokens last_fill_time 0.2 12 `gdate +%s%3N` 1
在這里插入圖片描述
妲己:lua 腳本的執(zhí)行會不會有性能上的損耗,比較redis是單線程的?
安琪拉:redis 使用 epoll 實(shí)現(xiàn)I/O多路復(fù)用的事件驅(qū)動(dòng)模型,對于每一個(gè)讀取和寫入操作都盡量要快速,所以我們需要對編寫的lua 腳本做個(gè)壓測,redis 提供了壓測指令 redis-benchmark, 測試10萬 腳本的執(zhí)行,命令如下:
redis-benchmark -n 100000 evalsha ebbcd2ed99990afaca6d2ba61a0f2d5bdd907e59 2 remain.${1}.tokens last_fill_time 0.2 12 `gdate +%s%3N` 1
實(shí)際效果如下:
99.9%都在 2ms以內(nèi)完成,每秒鐘執(zhí)行4萬5千多次,因此損耗可以接受。
妲己:怎么把分布式限流lua 放到Spring boot工程中呢?
安琪拉:下面我們就開始工程化之路,
首先
手寫一個(gè)
lua腳本(上面的腳本直接拷貝),在Spring 工程目錄中放好,如下圖;
程序啟動(dòng)時(shí)加載
lua腳本, 根據(jù)lua的 SHA1值判斷腳本是否已經(jīng)加載到redis( redis 不能存太多的script),程序如下:

這里程序啟動(dòng),加載腳本,檢查腳本在redis中是否存在,腳本如果沒有重新編輯更新,sha1是一致的,不會重復(fù)加載,另外注意一點(diǎn),如果是集群模式,Jedis 3.*版本以前不支持lua腳本,建議使用Lettuce。
關(guān)于Lettuce 和 Jedis 客戶端的對比,大家可以網(wǎng)上看一下,Spring Boot最新默認(rèn)客戶端已經(jīng)改成Lettuce了。
EvalSha is not supported in cluster environment
配置限流器

這里有一點(diǎn)需要注意一下,key 都帶了 {1} 的前綴,這個(gè)用于所有key 在集群模式都hash 命中同一個(gè)slot (槽),因?yàn)閘ua 腳本不能跨集群節(jié)點(diǎn)執(zhí)行。
看一下效果,舒服...:
各類知識點(diǎn)總結(jié)
下面的文章都有對應(yīng)的原創(chuàng)精美PDF,在持續(xù)更新中,可以來找我催更~
- 92頁的Mybatis
- 129頁的多線程
- 141頁的Servlet
- 158頁的JSP
- 76頁的集合
- 64頁的JDBC
- 105頁的數(shù)據(jù)結(jié)構(gòu)和算法
- 142頁的Spring
- 58頁的過濾器和監(jiān)聽器
- 30頁的HTTP
- 42頁的SpringMVC
- Hibernate
- AJAX
- Redis
- ......
掃碼或者微信搜Java3y 免費(fèi)領(lǐng)取原創(chuàng)思維導(dǎo)圖、精美PDF。在公眾號回復(fù)「888」領(lǐng)取,PDF內(nèi)容純手打有任何不懂歡迎來問我。
原創(chuàng)電子書
原創(chuàng)思維導(dǎo)圖

![]() |
|


