一 、業務場景php
在作項目系統接口服務的時候,爲了防止客戶端對於接口的濫用、保護服務器的資源, 一般來講咱們會對於服務器上的各類接口進行請求次數的限制。好比對於某個用戶,他在一個時間段內,好比 1 0秒,請求服務器接口的次數不可以大於一個上限,好比說5次。若是用戶調用接口的次數超過上限的話,就直接拒絕用戶的請求,返回錯誤信息。redis
服務接口的流量控制策略:限流、分流、降級、熔斷等。本文討論下限流策略,雖然下降了服務接口的訪問頻率和併發量,卻換取服務接口和業務應用系統的高可用。算法
2、經常使用的限流算法json
一、漏桶算法服務器
漏桶(Leaky Bucket)算法思路比較簡單,水(請求)先進入到漏桶裏,漏桶以必定的速度出水(接口有響應速率),當水流進速度過大會直接致使桶溢出(訪問頻率超過接口響應速率),而後就拒絕請求,這裏咱們能夠得出結論:漏桶算法能強行限制數據的傳輸速率。網絡
漏桶算法的話通常有兩個變量,一個是桶的大小,也就是你的承載量,好比流量突發增多時能夠存多少的水,另外一個就是你可以去正常處理的請求,水桶漏洞的大小。併發
由於漏桶的漏出速率是固定的,因此即便網絡中不存在資源衝突(沒有發生擁塞),漏桶算法也不能使流量突發到端口。所以,漏桶算法對於存在突發特性的流量來講缺少效率。分佈式
二、令牌桶算法ide
令牌桶算法(Token Bucket)和Leaky Bucket 效果同樣但方向相反的算法,隨着時間流逝,系統會按你的併發量,好比恆定1/QPS時間間隔(若是QPS=100,則間隔是10ms)往桶裏加入令牌,桶能夠承載必定的令牌數,具體一個桶有多少令牌,這裏是能夠本身去控制的。當請求進來時,每一個請求都會拿走一個Token,若是沒有Token拿了就阻塞等待或者拒絕服務。ui
Redis令牌桶算法還有一個優勢就是能夠方便的改變速度. 若是你須要提升速率,則按需提升放入桶中的令牌的速率. 通常會定時(好比10毫秒)往桶中增長必定數量的令牌, 有些算法則實時的計算應該增長的令牌的數量。
3、觀摩PHP+Redis實現令牌桶算法
<?php namespace Api\Li; //令牌桶限流算法 class Rate{ private $minSecond = 20; //單個用戶每分鐘訪問數 private $daySecond = 5000; //單個用戶天天總的訪問量 // public function minLi($uid){ $minSecondKey = $uid . '_minSecond'; $daySecondKey = $uid . '_daySecond'; $resMin = $this->getRedisLi($minSecondKey, $this->minSecond, 60); $resDay = $this->getRedisLi($minSecondKey, $this->minSecond, 86400); if (!$resMin['status'] || !$resDay['status']) { exit($resMin['msg'] . $resDay['msg']); } } public function getRedisLi($key, $initNum, $expire){ $nowtime = time(); $result = ['status' => true, 'msg' => '']; $redisObj = $this->di->get('redis'); $redis->watch($key); $limitVal = $redis->get($key); if ($limitVal) { $limitVal = json_decode($limitVal, true); $newNum = min($initNum, ($limitVal['num'] - 1) + (($initNum / $expire) * ($nowtime - $limitVal['time']))); if ($newNum > 0) { $redisVal = json_encode(['num' => $newNum, 'time' => time()]); } else { return ['status' => false, 'msg' => '目前令牌已經消耗完!']; } } else { $redisVal = json_encode(['num' => $initNum, 'time' => time()]); } $redis->multi(); $redis->set($key, $redisVal); $rob_result = $redis->exec(); if (!$rob_result) { $result = ['status' => false, 'msg' => '很抱歉,訪問次數過於頻繁!']; } return $result; }}
代碼要點:
(1)定義規則
單個用戶每分鐘訪問次數($minSecond),單個用戶天天總的訪問次數($daySecond)。
(2)計算速率
該代碼示例以秒爲最小的時間單位,速率=訪問次數/時間($initNum / $expire)
(3)每次消耗令牌後須要補充的令牌個數--計算方式
獲取上次請求的時間即上次存入令牌的時間,計算當前時刻與上次請求的時間差乘以速率就是這次須要補充的令牌個數,切記!補充令牌後的總令牌數不能大於初始化令牌數,以補充數和初始化數的最小值爲標準。
(4)程序流程
第一次請求時初始化令牌個數($minSecond),存入Redis同時將當前的時間戳存入以便計算下次須要補充的令牌個數。第二次訪問時獲取剩餘的令牌個數,並添加本次應該補充的令牌個數,補充後如何令牌數>0則當前訪問是有效的能夠訪問,不然令牌使用完畢不可訪問。先補充令牌再判斷令牌是否>0的緣由是因爲還有速率這個概念即若是上次剩餘的令牌爲0可是本次應該補充的令牌>1那麼本次依然能夠訪問。
(5)針對併發的處理
5.1使用樂觀鎖+事務解決。
5.2redis無需集合+list隊列
5.3redis+lua分佈式鎖