為什么要做限流
首先讓我們先看一看系統(tǒng)架構(gòu)設(shè)計中,為什么要做“限流”抽碌。
旅游景點通常都會有最大的接待量,不可能無限制的放游客進入决瞳,比如故宮每天只賣八萬張票货徙,超過八萬的游客,無法買票進入瞒斩,因為如果超過八萬人破婆,景點的工作人員可能就忙不過來,過于擁擠的景點也會影響游客的體驗和心情胸囱,并且還會有安全隱患祷舀;只賣N張票,這就是一種限流的手段烹笔。
軟件架構(gòu)中的服務(wù)限流也是類似裳扯,也是當系統(tǒng)資源不夠的時候,已經(jīng)不足以應(yīng)對大量的請求谤职,為了保證服務(wù)還能夠正常運行饰豺,那么按照規(guī)則,系統(tǒng)會把多余的請求直接拒絕掉允蜈,以達到限流的效果冤吨;
不知道大家注意過沒有,比如雙11饶套,剛過12點有些顧客的網(wǎng)頁或APP會顯示下單失敗的提示漩蟆,有些就是被限流掉了。
常見的限流算法
計數(shù)法
顧名思義就是來一個妓蛮,記錄一個怠李,比如我1分鐘只能處理1000個請求,那么我們就可以設(shè)置一個計數(shù)器,來一個請求就incr+1捺癞,當1分鐘之內(nèi)的數(shù)量大于等于1000之后不處理了即可夷蚊,偽代碼如下
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$rate_limit = 1000; //限制個數(shù)
$rate_seconds = 60; //限制時間
$redis_key = "redis_limit";
$count = $redis->get($redis_key);
if ($count >= $rate_limit){ //判斷60秒內(nèi)請求個數(shù)是否已經(jīng)達到上限
//直接返回,不處理請求
return
}
$redis->incr($redis_key, 1);//請求計數(shù)
$redis->expire($redis, $rate_seconds); //設(shè)置過期時間 60s
//to do 業(yè)務(wù)邏輯處理.......
這種計數(shù)方式比較簡單快捷髓介,但是有很大的缺點惕鼓,因為請求的訪問不一定是很平穩(wěn)的,如果0:59過來了1000個請求唐础,1:01已經(jīng)是下一個窗口呜笑,又過來了1000個請求,但實際上三秒內(nèi)來了2000個請求彻犁,已經(jīng)超過我們的限流上限了。所以這種方法是不推薦的凰慈。
滑動窗口算法
還拿上面的例子汞幢,一分鐘分6份,每份10秒微谓;每過10秒鐘渊鞋,我們的時間窗口就會往右滑動一格鼻种,每個格子都有獨立的計數(shù)器,我們每次都計算時間窗口內(nèi)的數(shù)量,可以解決計數(shù)器法中的問題辽社,而且當滑動窗口的格子越多,那么限流的統(tǒng)計就會越精確贫堰。具體可以參考下圖缕溉,看圖比較清晰
偽代碼實現(xiàn)如下
function api_limit($scene, $period, $maxCount){
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$key = sprintf('hist:%s', $scene); //限流場景唯一標識
$now = msectime(); // 毫秒時間戳,這樣更精確
$pipe=$redis->multi(Redis::PIPELINE); //使用管道提升性能
$pipe->zadd($key, $now, $now); //value 和 score 都使用毫秒時間戳
$pipe->zremrangebyscore($key, 0, $now - $period); //移除時間窗口之前的行為記錄,剩下的都是時間窗口內(nèi)的
$pipe->zcard($key); //獲取窗口內(nèi)的行為數(shù)量
$pipe->expire($key, $period/1000 + 1); //多加一秒過期時間
$replies = $pipe->exec();
return $replies[2] <= $maxCount; //$replies[2]為zcard返回的個數(shù) 如果zcard結(jié)果大于maxCount肴焊,則不處理結(jié)果
}
for ($i=0; $i<20; $i++){ //測試限流是否實現(xiàn)代碼
var_dump(isActionAllowed("uniq_scene", 60*1000, 5)); //執(zhí)行可以發(fā)現(xiàn)只有前5次是通過的
}
//返回當前的毫秒時間戳
function msectime() {
list($msec, $sec) = explode(' ', microtime());
$msectime = (float)sprintf('%.0f', (floatval($msec) + floatval($sec)) * 1000);
return $msectime;
}
這段代碼還是略顯復(fù)雜前联,需要讀者花一定的時間好好啃。它的整體思路就是:每一個行為到來時娶眷,都維護一次時間窗口似嗤。將時間窗口外的記錄全部清理掉,只保留窗口內(nèi)的記錄届宠。
因為這幾個連續(xù)的 Redis 操作都是針對同一個 key 的烁落,使用 pipeline 可以顯著提升Redis 存取效率。但這種方案也有缺點豌注,因為它要記錄時間窗口內(nèi)所有的行為記錄伤塌,如果這個量很大,比如限定 60s 內(nèi)操作不得超過 100w 次這樣的參數(shù)幌羞,它是不適合做這樣的限流的寸谜,因為會消耗大量的存儲空間。
后面還有漏桶算法和令牌桶算法,由于各自的實現(xiàn)比較復(fù)雜熊痴,所以準備各自新開一篇文章單獨描述