沐鳴平台註冊登錄_【PHP】用Redis實現限流的常見方案
限流實現的思路比較多,一般比較常見的思路有 計數器,滑動窗口,令牌桶。
而Redis有着豐富的數據結構以及分佈式的支持,使用Redis實現限流的業務還是比較適合的。
並且在Redis 4.0 上可以安裝限流模塊 redis-cell,其思路也是令牌桶,其提供了限流的原子操作使用起來很方便可靠。
計數器
計數器即在限定的時間內記請求的次數,如果過了這個時間段就重置次數。
這和我們平時參加一些活動很像,比如超市裡有時做活動,每天一個人可以領一個雞蛋,今天領過了就不能再領了但到了明天又可以再領了。
這裏可以用 String 來實現,比如要實現一分鐘限制100次,只要記下上次請求的是哪一分鐘,並記住這一分鐘請求的次數即可,利用過期時間可以淘汰不用的數據。
public function checkRequest($uid)
{
$timeKey = self::LIMIT_TIME_KEY . $uid;
// 得到上次請求的時間
$lastTime = $this->redis->get($timeKey);
if ($lastTime) {
// 看看時間戳是不是已經過了一分鐘(自己設定的時間範圍)
$nowTime = time();
if ($nowTime - $lastTime < self::LIMIT_UNIT) {
// 還沒一分鐘
// 獲取下請求了多少次
$countKey = self::LIMIT_KEY . $lastTime . ‘:‘ . $uid;
$count = $this->redis->get($countKey);
if ($count >= self::LIMIT_COUNT) {
// 超過次數了
return false;
} else {
// 沒超過加一
$this->redis->set($countKey, ++$count, self::TTL);
}
} else {
// 超過一分鐘了可以重置了
$this->initLimitData($uid);
}
} else {
// 都沒有記錄肯定沒請求或好久沒請求了 可以重置了
$this->initLimitData($uid);
}
return true;
}
這種實現缺點就是如果請求正好在間隔處密集請求,就大致可以被請求兩倍的限額量。
滑動窗口
這種思路是記錄下每次請求的時間,然後再統計下在規定範圍內(窗口內)被請求了多少次。
這種方式可以通過Redis ZSET 有序集合來實現,通過時間戳作為分數,只要獲取相應時間戳範圍內的請求次數並加以判斷即可。
public function checkRequest($uid)
{
// 是否嚴重超過請求限制
$banKey = self::LIMIT_BAN_KEY . $uid;
$isBan = $this->redis->get($banKey);
if ($isBan) {
return false;
}
// 得到微秒 粒度太粗不好測
// startTime 與 nowTime 形成了一個窗口的時間範圍
$nowTime = microtime(true) * 1000;
$startTime = $nowTime - self::LIMIT_UNIT * 1000;
$zSetKey = self::LIMIT_KEY . $uid;
// 獲取時間範圍內的請求數據
$requestHistory = $this->redis->zRangeByScore($zSetKey, $startTime, $nowTime);
$count = count($requestHistory);
// 首先判斷是否嚴重超過限制
if ($count > self::LIMIT_BAN_COUNT) {
// 直接封
$this->redis->set($banKey, 1, self::BAN_TTL);
return false;
}
// 將下面的REDIS命令打包,使一組命令具有原子性
$this->redis->multi();
// 添加
$options = [];
$value = $uid . ‘:‘ . $nowTime . rand(0, 999);
// 多餘的數據刪除
$this->redis->zRemRangeByScore($zSetKey, 0, $startTime);
// 添加數據
$this->redis->zAdd($zSetKey, $options, $nowTime, $value);
// 設置過期時間
$this->redis->expire($zSetKey, self::TTL);
// 執行這一組命令
$this->redis->exec();
if ($count >= self::LIMIT_COUNT) {
return false;
} else {
return true;
}
}
要注意的是如果不清理窗口外的數據並有用戶一直穩定地請求的話,這個用戶的有序集合就會越來越大。
並且刪除時也需要注意是否待刪除數據會不會很多,因為刪除一個BigKey是有可能造成Redis短暫的阻塞。
可以判斷下用戶在窗口內的請求次數有沒有嚴重超過限制的次數,嚴重超過後可以考慮停用該用戶的服務。
令牌桶
這種限流方式很像小學時候遇到的一種數學題,有個水池如果打開水龍頭N小時裝滿,如果打開下面的水塞M小時放完,現在同時打開水龍頭和水塞問啥時候水能放完。
哈哈,如果要把水放完當然要流出的水比流入的水速率高才行。
令牌桶也是一樣就是不斷往桶里生成令牌,取的太快,取完了就會限流。
用Redis實現只要記錄下每次請求的時間,計算這次與上次請求的兩次時間內應該生成多少個令牌再減去這次消耗的一個令牌 如果算下來令牌小於0就應該限流了。
public function checkRequest($uid)
{
$key = self::LIMIT_KEY . $uid;
$data = $this->redis->get($key);
// 是否有請求的記錄數據
if ($data) {
$value = json_decode($data, true);
// 計算一下上個請求到現在為止會生成多少個令牌
$nowTime = microtime(true) * 1000;
$speed = self::LIMIT_COUNT / (self::LIMIT_UNIT * 1000);
// 時間間隔不要太長
$timeGap = min([$nowTime - $value[‘time‘], self::MAX_GAP * 1000]);
$tokenCount = $timeGap * $speed;
// 之前的數量 加上生成的數量 再減掉這次消耗的1個
$count = $value[‘count‘] + $tokenCount - 1;
// 記錄下令牌的變動
$this->redis->set($key, json_encode($this->getJsonData($count)), self::TTL);
if ($count <= 0) {
return false;
} else {
return true;
}
} else {
// 設置初始時間信息
$initData = $this->getJsonData(1);
$this->redis->set($key, json_encode($initData), self::TTL);
return true;
}
}
令牌桶是根據速率限流,所以會很敏感,因為生成的速率是平均的,如果一開始流量就突然很大就很容易被限流。當然根據不同的業務場景和需求可以因地制宜的修改與實現。
站長推薦
1.雲服務推薦: 國內主流雲服務商,各類雲產品的最新活動,優惠券領取。地址:阿里雲騰訊雲華為雲
2.廣告聯盟: 整理了目前主流的廣告聯盟平台,如果你有流量,可以作為參考選擇適合你的平台點擊進入
鏈接: http://www.fly63.com/article/detial/9965