程序員如何應對雙十一購物的大流量衝擊?
作者 | LLLSQ
責編| 胡巍巍
雙十一就要來了!在雙十一這樣的大流量場景中,搶購、下單量會非常大,如果業務應用系統的負載能力有限,非預期的請求,就會給系統帶來很大壓力,從而拖垮業務應用系統。
那麼,在面對大流量時,該如何進行流量控制?
服務介面的流量控制策略:分流、降級、限流等。本文討論下限流策略,雖然降低了服務介面的訪問頻率和並發量,卻換取服務介面和業務應用系統的高可用。
限流演算法
常用的限流演算法有漏桶演算法、令牌桶演算法。
1、漏桶演算法
漏桶(Leaky Bucket)演算法思路很簡單,水(請求)先進入到漏桶里,漏桶以一定的速度出水(介面有響應速率),當水流入速度過大會直接溢出(訪問頻率超過介面響應速率),然後就拒絕請求,可以看出漏桶演算法能強行限制數據的傳輸速率。示意圖如下:
可見這裡有兩個變數,一個是桶的大小,支持流量突發增多時可以存多少的水(burst),另一個是水桶漏洞的大小(rate)。
因為漏桶的漏出速率是固定的參數,所以,即使網路中不存在資源衝突(沒有發生擁塞),漏桶演算法也不能使流突發(burst)到埠速率。因此,漏桶演算法對於存在突發特性的流量來說缺乏效率。
2、令牌桶演算法
令牌桶演算法(Token Bucket)和 Leaky Bucket 效果一樣但方向相反的演算法,更加容易理解。
隨著時間流逝,系統會按恆定1/QPS時間間隔(如果QPS=100,則間隔是10ms)往桶里加入Token(想像和漏洞漏水相反,有個水龍頭在不斷的加水),如果桶已經滿了就不再加了。新請求來臨時,會各自拿走一個Token,如果沒有Token可拿了就阻塞或者拒絕服務。
令牌桶的另外一個好處是可以方便的改變速度。一旦需要提高速率,則按需提高放入桶中的令牌的速率。一般會定時(比如100毫秒)往桶中增加一定數量的令牌, 有些變種演算法則實時的計算應該增加的令牌的數量。
ReteLimiter完成限流、搶購場景實現
RateLimiter是Guava提供的基於令牌桶演算法的實現類,API使用簡單,並且根據系統的實際情況來調整生成Token的速率。
maven引入:
com.google.guava
guava
18.0
代碼:
packagecom.lsq.einterview;
importcom.google.common.util.concurrent.RateLimiter;
publicclassRateLimiterTest{
/*
簡單使用Ratelimiter 實現限流功能,
例:限制2秒鐘只能有一個任務通過
*/
publicstaticvoidmain(String[] args){
//每秒通過0.5,所以2秒只能有一個通過
RateLimiter rateLimiter = RateLimiter.create(0.5);
for(inti =; i
doubleacquire = rateLimiter.acquire();
System.out.println("任務執行,等待時間:"+acquire);
}
}
}
結果:
rateLimiter.acquire()返回的是本次執行等待的時間,我們可以清楚地看到,第一次無需等待,後邊都需要等待兩秒才可以執行到,
rateLimiter.acquire()該方法會阻塞線程,直到令牌桶中能取到令牌為止才繼續向下執行。
搶購實現1
我們模擬搶購商品,總共有35件商品,模擬我們伺服器只能承受住20個並發,超過20個伺服器會掛掉,我們用RateLimiter來進行限流,
我們沒有傳參數,當然實際場景複雜很多。
packagecom.lsq.einterview.controller;
importcom.google.common.util.concurrent.RateLimiter;
importcom.lsq.einterview.domain.APIResponse;
importorg.springframework.web.bind.annotation.RequestMapping;
importorg.springframework.web.bind.annotation.RestController;
@RestController()
@RequestMapping("/api")
publicclassRushBuyController{
privatestaticfinalRateLimiter rateLimiter = RateLimiter.create(20);
privatestatic int productCount=35;
privatestaticfinalObject o=new Object();
/**
* 我們模擬搶購商品,模擬我們伺服器只能承受住20個並發,超過10個伺服器會掛掉,
* 我們用rateLimiter來進行限流,
* 我們也沒有傳參數,當然實際場景複雜很多。
*@return響應體
*/
@RequestMapping(value ="/rushBuy2Product", produces ="application/json;charset=utf-8")
publicAPIResponse rushBuy2Product() {
System.out.println("進入搶購:等待時間為:"+ rateLimiter.acquire());
boolean b = productStock();
if(b){
System.out.println("搶購成功");
}else{
System.out.println("搶購失敗");
}
returnnew APIResponse().success();
}
/**
* 搶購商品的庫存
*@return
*/
privateboolean productStock(){
if(productCount==){
returnfalse;
}
synchronized (o){
if(productCount>){
--productCount;
System.out.println("剩餘庫存為:"+productCount);
returntrue;
}else{
returnfalse;
}
}
}
}
使用Jmeter測試開啟100個線程進行測試:
統計下商品搶購成功的數據,正好是35個搶購成功。
我們看到在開始進入的請求不需要等待,直接執行,後面進來的需要等待的時間慢慢變長,因為拿不到令牌。
但是這種方法有個很明顯的缺陷,在實際並不適合使用,因為用戶請求進來,拿不到令牌就需要等待執行,體驗十分差。所以下面我們介紹另一種方式。
搶購實現2
由於RateLimiter是屬於單位時間內生成多少個令牌的方式,譬如0.1秒生成1個,那搶購就要看運氣了,你剛好是在剛生成1個時進來了。
那麼你就能搶到,在這0.1秒內其他的請求就算白瞎了,只能寄希望於下一個0.1秒,而從用戶體驗上來說,不能讓他在那一直阻塞等待。
所以就需要迅速判斷,該用戶在某段時間內,還有沒有機會得到令牌,這裡就需要使用TryAcquire(long timeout、TimeUnit、Unit)方法,指定一個超時時間,一旦判斷出在timeout時間內還無法取得令牌,就返回false。
注意,這裡並不是真正的等待了timeout時間,而是被判斷為即便過了timeout時間,也無法取得令牌。這個是不需要等待的。
packagecom.lsq.einterview.controller;
importcom.google.common.util.concurrent.RateLimiter;
importcom.lsq.einterview.domain.APIResponse;
importorg.springframework.web.bind.annotation.RequestMapping;
importorg.springframework.web.bind.annotation.RestController;
importjava.util.concurrent.TimeUnit;
@RestController()
@RequestMapping("/api")
publicclassRushBuyController{
privatestaticfinalRateLimiter rateLimiter = RateLimiter.create(20);
privatestatic int productCount=35;
privatestaticfinalObject o=new Object();
/**
* 我們模擬搶購商品,模擬我們伺服器只能承受住20個並發,超過10個伺服器會掛掉,
* 我們用rateLimiter來進行限流,
* 我們也沒有傳參數,當然實際場景複雜很多。
*@return響應體
*/
@RequestMapping(value ="/rushBuy2Product", produces ="application/json;charset=utf-8")
publicAPIResponse rushBuy2Product() {
boolean isAcquire = rateLimiter.tryAcquire(1, TimeUnit.SECONDS);
if(!isAcquire){
System.out.println("進入搶購:在1s內無法拿到令牌,直接返回");
returnnew APIResponse().fail("搶購失敗");
}
boolean b = productStock();
if(b){
System.out.println("搶購成功");
}else{
System.out.println("搶購失敗");
}
returnnew APIResponse().success();
}
/**
* 搶購商品的庫存
*@return
*/
privateboolean productStock(){
if(productCount==){
returnfalse;
}
synchronized (o){
if(productCount>){
--productCount;
System.out.println("剩餘庫存為:"+productCount);
returntrue;
}else{
returnfalse;
}
}
}
}
結果:
在我們的所有日誌文件中統計:
搶購成功:35;
搶購失敗:23;
無法拿到令牌,未參與搶購:42;
總數為我們100個線程。
總結
搶購實現的方式有很多種,在搶購2案例中,按照固定的單位時間進行分割,每個單位時間產生一個令牌,可供購買。
我們可以想到平常參與搶購秒殺時,有時候你網速很快,但總是搶不到,現在明白了吧。
真正的搶購不是這麼簡單,難度也複雜得多。所以不只是在代碼中限流能實現的。
瞬間的流量洪峰會衝垮伺服器的負載,當幾十萬人搶購幾件商品時,連介面都請求不進來,更別提介面里的令牌分配了。
作者:LLLSQ,一隻有著悲慘故事的北漂程序員,為讀者提供熱點技術文章和IT實時熱點新聞、架構、面試信息等最新訊息。
聲明:本文為作者投稿,版權歸其個人所有。
-- END --
微信改版了,
想快速看到CSDN的熱乎文章,
趕快把CSDN公眾號設為星標吧,
打開公眾號,點擊「設為星標」就可以啦!


※BAT 把持的小程序領地,現在入場的今日頭條還有救嗎?
※微軟曾經的二號人物永遠地離開了
TAG:CSDN |