令牌桶限流思路分享(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
來源:簡書


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM