PHP+Redis令牌桶限流

一 、業務場景


  在做項目系統接口服務的時候,爲了防止客戶端對於接口的濫用、保護服務器的資源, 通常來說我們會對於服務器上的各種接口進行請求次數的限制。比如對於某個用戶,他在一個時間段內,比如 1 0秒,請求服務器接口的次數不能夠大於一個上限,比如說5次。如果用戶調用接口的次數超過上限的話,就直接拒絕用戶的請求,返回錯誤信息。

  服務接口的流量控制策略:限流、分流、降級、熔斷等。本文討論下限流策略,雖然降低了服務接口的訪問頻率和併發量,卻換取服務接口和業務應用系統的高可用。


二、常用的限流算法

  

  1、漏桶算法

  漏桶(Leaky Bucket)算法思路比較簡單,(請求)先進入到漏桶裏,漏桶以一定的速度出水(接口有響應速率),當水流進速度過大會直接導致桶溢出(訪問頻率超過接口響應速率),然後就拒絕請求,這裏我們可以得出結論:漏桶算法能強行限制數據的傳輸速率。

  漏桶算法的話一般有兩個變量,一個是桶的大小,也就是你的承載量,比如流量突發增多時可以存多少的水,另一個就是你能夠去正常處理的請求,水桶漏洞的大小。

因爲漏桶的漏出速率是固定的,所以即使網絡中不存在資源衝突(沒有發生擁塞),漏桶算法也不能使流量突發到端口。因此,漏桶算法對於存在突發特性的流量來說缺乏效率。


  2、令牌桶算法

  令牌桶算法(Token Bucket)和Leaky Bucket 效果一樣但方向相反的算法,隨着時間流逝,系統會按你的併發量,比如恆定1/QPS時間間隔(如果QPS=100,則間隔是10ms)往桶裏加入令牌,桶可以承載一定的令牌數,具體一個桶有多少令牌,這裏是可以自己去控制的。當請求進來時,每個請求都會拿走一個Token,如果沒有Token拿了就阻塞等待或者拒絕服務。

圖片


  Redis令牌桶算法還有一個優點就是可以方便的改變速度如果你需要提高速率,則按需提高放入桶中的令牌的速率一般會定時(比如10毫秒)往桶中增加一定數量的令牌有些算法則實時的計算應該增加的令牌的數量。


三、觀摩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分佈式鎖


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章