前景回顧:
《基于計數(shù)器的服務(wù)接口限流實例》
一、RateLimit中acquire的使用
在前面這篇文章中阶祭,我們使用了計數(shù)器來做服務(wù)接口的限流绎签,它最大的問題在于岛蚤,無法將請求均勻地分攤到單位時間內(nèi)的每個時間段上。行業(yè)術(shù)語就是無法平滑地分攤請求到涂。
本文的主角Guava中的RateLimiter就可以很好地平滑地分攤請求脊框。關(guān)于RateLimiter所涉及的漏桶及令牌桶算法原理,本文不再贅述践啄,可以參考文末的參考文章浇雹。
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.0-jre</version>
</dependency>
RateLimiter的使用非常簡單,如下是最基本的使用實例:
@Slf4j
@RestController
public class UserMailRest {
/**
* 每秒投入2個令牌
* */
private RateLimiter rateLimiter = RateLimiter.create(2);
@GetMapping("/getUserMail")
public String getUserMail(){
rateLimiter.acquire();
log.info("請求得到了服務(wù)屿讽!");
return "OK";
}
}
如上代碼的作用就是規(guī)定每秒產(chǎn)生2個令牌昭灵,控制一秒內(nèi)最多只能有2個請求得到服務(wù)。按照RateLimiter的設(shè)計原則伐谈,這兩個令牌的投放時間間隔為1/2=0.5秒烂完,如此,能達到每隔0.5秒接受一個請求對其服務(wù)的目的衩婚。
我們使用Jmeter作為測試工具窜护,設(shè)置10個用戶線程,在一秒內(nèi)各自發(fā)起一次請求非春。
得到的結(jié)果如下:
2019-12-23 21:32:44.714 : 請求得到了服務(wù)柱徙!
2019-12-23 21:32:44.809 : 請求得到了服務(wù)!
2019-12-23 21:32:44.910 : 請求得到了服務(wù)奇昙!
2019-12-23 21:32:45.217 : 請求得到了服務(wù)护侮!
2019-12-23 21:32:45.714 : 請求得到了服務(wù)!
2019-12-23 21:32:46.264 : 請求得到了服務(wù)储耐!
2019-12-23 21:32:46.714 : 請求得到了服務(wù)羊初!
2019-12-23 21:32:47.214 : 請求得到了服務(wù)!
2019-12-23 21:32:47.714 : 請求得到了服務(wù)!
2019-12-23 21:32:48.214 : 請求得到了服務(wù)长赞!
現(xiàn)在模擬下這10個請求發(fā)起后晦攒,服務(wù)端都發(fā)生了什么:
- 40秒,服務(wù)器啟動得哆,此時沒有請求到來脯颜,服務(wù)器中積累了2個令牌;
- 40秒~44秒贩据,此時還是沒有請求到來栋操,服務(wù)器每秒都會產(chǎn)生2個令牌,但是因為前面存儲的令牌沒有被消費饱亮,而當前最多只能存放2個令牌矾芙,所以在此期間產(chǎn)生的令牌全部被放棄;
- 44秒714的時候近上,第一個請求到來剔宪,此時服務(wù)器已經(jīng)存放了兩個歷史令牌,所以請求不用等待戈锻,直接獲取令牌得到服務(wù)歼跟;
- 44秒809,第二個請求到來格遭,同樣立馬得到服務(wù)哈街;
- 44秒910,第三個請求獲取了44秒下半秒本來就應該產(chǎn)生的一個令牌拒迅,因此也能立馬得到服務(wù)骚秦;
- 45秒217,第四個請求獲取了45秒上半秒本來就應該產(chǎn)生的一個令牌...
以此類推璧微,可以看到作箍,我們原本在1秒內(nèi)發(fā)起的10個請求,在5秒內(nèi)前硫,差不多每隔0.5秒被服務(wù)接受胞得。從而使得請求的時間分布變得平滑,不會在某一個毫秒內(nèi)集中被服務(wù)屹电。
只不過需要注意的是阶剑,RateLimiter會存儲空閑的令牌,但是最多只能存儲一個時間單位的令牌數(shù)目危号,從而會使得空閑后突然激增的請求也能得到服務(wù)牧愁。
二、RateLimiter中tryAcquire的使用
acquire的使用是為了將所有的請求平滑地分布到后續(xù)的時間段內(nèi)外莲,但有的時候猪半,請求實在太多了,我們需要對調(diào)用過于頻繁的請求給予拒絕服務(wù)的響應,此時就需要用到tryAcquire了磨确。
@Slf4j
@RestController
public class UserMailRest {
/**
* 每秒投入2個令牌
* */
private RateLimiter rateLimiter = RateLimiter.create(2);
@GetMapping("/getUserMail")
public String getUserMail(){
if(!rateLimiter.tryAcquire()){
log.warn("請求過于頻繁沽甥,拒絕服務(wù)!");
return "請求過于頻繁俐填,拒絕服務(wù)安接!";
}
log.info("請求得到服務(wù)!");
return "OK";
}
}
如上只有在tryAcquire失敗的情況下英融,拒絕服務(wù),執(zhí)行結(jié)果如下:
2019-12-23 21:57:32.083 : 請求得到服務(wù)歇式!
2019-12-23 21:57:32.083 : 請求得到服務(wù)驶悟!
2019-12-23 21:57:32.126 : 請求得到服務(wù)!
2019-12-23 21:57:32.224 : 請求過于頻繁材失,拒絕服務(wù)痕鳍!
2019-12-23 21:57:32.324 : 請求過于頻繁,拒絕服務(wù)龙巨!
2019-12-23 21:57:32.426 : 請求過于頻繁笼呆,拒絕服務(wù)!
2019-12-23 21:57:32.526 : 請求過于頻繁旨别,拒絕服務(wù)诗赌!
2019-12-23 21:57:32.626 : 請求得到服務(wù)!
2019-12-23 21:57:32.726 : 請求過于頻繁秸弛,拒絕服務(wù)铭若!
2019-12-23 21:57:32.829 : 請求過于頻繁,拒絕服務(wù)递览!
同樣的叼屠,我們分析下服務(wù)端都發(fā)生了什么:
- 30秒,服務(wù)器啟動绞铃,此時沒有請求到來镜雨,服務(wù)器中積累了2個令牌;
- 30秒~32秒儿捧,此時還是沒有請求到來荚坞,服務(wù)器每秒都會產(chǎn)生2個令牌,但是因為前面存儲的令牌沒有被消費纯命,而當前最多只能存放2個令牌西剥,所以在此期間產(chǎn)生的令牌全部被放棄;
- 32秒083的時候亿汞,第一個請求到來瞭空,此時服務(wù)器已經(jīng)存放了兩個歷史令牌,所以請求不用等待,直接獲取令牌得到服務(wù)咆畏;
- 32秒083南捂,第二個請求到來,同樣立馬得到服務(wù)旧找;
- 32秒126溺健,第三個請求獲取了32秒上半秒本來就應該產(chǎn)生的一個令牌,因此也能立馬得到服務(wù)钮蛛;
- 32秒224~32秒526鞭缭,這些請求都沒有令牌可以獲取,因為此時魏颓,32秒下半秒的令牌還沒有被釋放岭辣,因此,全部都拒絕服務(wù)甸饱;
- 32秒626沦童,第八個請求獲得了32秒下半秒本來就應該產(chǎn)生的令牌,因此得到服務(wù)叹话;
- 剩下的兩個請求都是在32秒下半秒發(fā)生的偷遗,因此獲取不到令牌,因此拒絕服務(wù)驼壶;
三氏豌、總結(jié)
好了,關(guān)于RateLimiter的常見使用方法主要就是以上講解的acquire以及tryAcquire辅柴,通過實例后的分析我們大致也能了解其工作原理箩溃。
RateLimiter相較于計數(shù)器的限流方案來說,最大的特點就是限制請求在單位時間內(nèi)平滑地對服務(wù)器進行訪問碌嘀,從而不會發(fā)生涣旨,在一個毫秒內(nèi)爆發(fā)全部的請求,瞬間壓垮服務(wù)的情況股冗。
但是霹陡,和計數(shù)器一樣,存在一個很致命的問題止状。它們都只是限制單位時間內(nèi)請求的數(shù)量烹棉,但并不能限制服務(wù)器上并發(fā)的數(shù)量。比如怯疤,1秒內(nèi)允許200個訪問浆洗,但是這200個訪問是很耗時的LongCall,從而導致第n秒的時候集峦,服務(wù)器上可能就會有200*n個線程的并發(fā)伏社,最終超過系統(tǒng)能承載的最大并發(fā)數(shù)抠刺,壓垮系統(tǒng)。