1. 程式人生 > 實用技巧 >令牌桶限流思路分享(PHP+Redis實現機制)

令牌桶限流思路分享(PHP+Redis實現機制)

一 、場景描述

在開發介面伺服器的過程中,為了防止客戶端對於介面的濫用,保護伺服器的資源, 通常來說我們會對於伺服器上的各種介面進行呼叫次數的限制。比如對於某個 使用者,他在一個時間段(interval)內,比如 1 分鐘,呼叫伺服器介面的次數不能夠 大於一個上限(limit),比如說 100 次。如果使用者呼叫介面的次數超過上限的話,就直接拒絕使用者的請求,返回錯誤資訊。
服務介面的流量控制策略:分流、降級、限流等。本文討論下限流策略,雖然降低了服務介面的訪問頻率和併發量,卻換取服務介面和業務應用系統的高可用。

二、常用的限流演算法

1、漏桶演算法
漏桶(Leaky Bucket)演算法思路很簡單,水(請求)先進入到漏桶裡,漏桶以一定的速度出水(介面有響應速率),當水流入速度過大會直接溢位(訪問頻率超過介面響應速率),然後就拒絕請求,可以看出漏桶演算法能強行限制資料的傳輸速率.示意圖如下:

   
可見這裡有兩個變數,一個是桶的大小,支援流量突發增多時可以存多少的水(burst),另一個是水桶漏洞的大小(rate)。
因為漏桶的漏出速率是固定的引數,所以,即使網路中不存在資源衝突(沒有發生擁塞),漏桶演算法也不能使流突發(burst)到埠速率.因此,漏桶演算法對於存在突發特性的流量來說缺乏效率.

2、令牌桶演算法
令牌桶演算法(Token Bucket)和 Leaky Bucket 效果一樣但方向相反的演算法,更加容易理解.隨著時間流逝,系統會按恆定1/QPS時間間隔(如果QPS=100,則間隔是10ms)往桶裡加入Token(想象和漏洞漏水相反,有個水龍頭在不斷的加水),如果桶已經滿了就不再加了.新請求來臨時,會各自拿走一個Token,如果沒有Token可拿了就阻塞或者拒絕服務.


令牌桶的另外一個好處是可以方便的改變速度. 一旦需要提高速率,則按需提高放入桶中的令牌的速率. 一般會定時(比如100毫秒)往桶中增加一定數量的令牌, 有些變種演算法則實時的計算應該增加的令牌的數量.

三、基於PHP+Redis實現的令牌桶演算法

<?php
namespace Api\Lib;

/**
 * 限流控制
 */
class RateLimit
{
    private $minNum = 60; //單個使用者每分訪問數
    private $dayNum = 10000; //單個使用者每天總的訪問量

    public function minLimit($uid)
    {
        $minNumKey = $uid . '_minNum';
        $dayNumKey = $uid . '_dayNum';
        $resMin    = $this->getRedis($minNumKey, $this->minNum, 60);
        $resDay    = $this->getRedis($minNumKey, $this->minNum, 86400);
        if (!$resMin['status'] || !$resDay['status']) {
            exit($resMin['msg'] . $resDay['msg']);
        }
    }

    public function getRedis($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:首先定義規則
單個使用者每分鐘訪問次數($minNum),單個使用者每天總的訪問次數($dayNum),介面總的訪問次數等不同的規則。
2:計算速率
該程式碼示例以秒為最小的時間單位,速率=訪問次數/時間($initNum / $expire)
3:每次訪問後補充的令牌個數計算方式
獲取上次訪問的時間即上次存入令牌的時間,計算當前時刻與上次訪問的時間差乘以速率就是此次需要補充的令牌個數,注意補充令牌後總的令牌個數不能大於初始化的令牌個數,以補充數和初始化數的最小值為準。
4:程式流程
第一次訪問時初始化令牌個數($minNum),存入Redis同時將當前的時間戳存入以便計算下次需要補充的令牌個數。第二次訪問時獲取剩餘的令牌個數,並新增本次應該補充的令牌個數,補充後如何令牌數>0則當前訪問是有效的可以訪問,否則令牌使用完畢不可訪問。先補充令牌再判斷令牌是否>0的原因是由於還有速率這個概念即如果上次剩餘的令牌為0但是本次應該補充的令牌>1那麼本次依然可以訪問。
5:針對併發的處理
使用Redis的樂觀鎖機制

四、Redis樂觀鎖介紹

redis對事務的支援比較簡單。redis只能保證一個客戶端發起的事務命令可以執行,中間不會插入其他事務。因為redis是單執行緒的,所以做到上面這點很容易。一般redis接受到客戶端的命令後會立即執行,但是如果客戶端發起multi命令,redis不會立即執行,而是讓當前連線進入事務上下文,把命令放到佇列中,接受到exec命令後,redis會順序執行佇列中的命令。並把執行結果打包到一起返回客戶端,之後就結束了事務上下文。
一、簡單的事務控制



這個例子可以看到:兩個set命令發出後並沒有立即執行而是放到佇列中,redis接受到exec命令才開始執行。
如果有兩個執行緒同時修改了一個變數的值,如何控制事務回滾?下面看樂觀鎖怎麼控制的?
二、樂觀鎖控制事務
1.什麼是樂觀鎖?
大多是基於資料版本的記錄機制。什麼是資料版本?就是為資料增加一個版本標識,即為資料庫表新增一個version欄位,當讀取資料時,把資料庫版本一同讀出,當做了修改後,將資料庫版本+1,同修改一起提交。如果提交資料的版本號 >資料庫當前版本號,提交成功。如圖:



2.樂觀鎖例項
假設資料庫中賬戶資訊表中有一個version欄位,當前值為1,賬戶餘額為$500

這樣避免了操作員B用舊資料修改表中記錄的的可能。
3.在redis中怎麼體現的?
redis中用watch監視key,如果key在提交前被修改,則提交不成功。如下:



當session1還沒來得及對age進行修改,session2已經將age的值設為30,session1再執行的時候失敗,因為session1對age加了樂觀鎖的緣故。
watch命令會監視key,當exec時如果監視的key從呼叫watch後發生過變化,則整個事務會失敗。也可以呼叫watch多次監視多個key。
三、redis事務存在的問題
redis保證事務中的命令連續執行,但是如果其中一條命令執行失敗,事務並不回滾。


為age +1的命令成功,因為anme是string型別的,所以不能做加操作,命令有一個失敗也不會回滾,age的值已經被修改了。
作者:dreamer_lk
連結:https://www.jianshu.com/p/9f76dd2757c7
來源:簡書