一 、場景描述
在開發接口服務器的過程當中,爲了防止客戶端對於接口的濫用,保護服務器的資源, 一般來講咱們會對於服務器上的各類接口進行調用次數的限制。好比對於某個 用戶,他在一個時間段(interval)內,好比 1 分鐘,調用服務器接口的次數不可以 大於一個上限(limit),好比說 100 次。若是用戶調用接口的次數超過上限的話,就直接拒絕用戶的請求,返回錯誤信息。
服務接口的流量控制策略:分流、降級、限流等。本文討論下限流策略,雖然下降了服務接口的訪問頻率和併發量,卻換取服務接口和業務應用系統的高可用。php
2、經常使用的限流算法
一、漏桶算法
漏桶(Leaky Bucket)算法思路很簡單,水(請求)先進入到漏桶裏,漏桶以必定的速度出水(接口有響應速率),當水流入速度過大會直接溢出(訪問頻率超過接口響應速率),而後就拒絕請求,能夠看出漏桶算法能強行限制數據的傳輸速率.示意圖以下:laravel
可見這裏有兩個變量,一個是桶的大小,支持流量突發增多時能夠存多少的水(burst),另外一個是水桶漏洞的大小(rate)。
由於漏桶的漏出速率是固定的參數,因此,即便網絡中不存在資源衝突(沒有發生擁塞),漏桶算法也不能使流突發(burst)到端口速率.所以,漏桶算法對於存在突發特性的流量來講缺少效率.面試
二、令牌桶算法
令牌桶算法(Token Bucket)和 Leaky Bucket 效果同樣但方向相反的算法,更加容易理解.隨着時間流逝,系統會按恆定1/QPS時間間隔(若是QPS=100,則間隔是10ms)往桶裏加入Token(想象和漏洞漏水相反,有個水龍頭在不斷的加水),若是桶已經滿了就再也不加了.新請求來臨時,會各自拿走一個Token,若是沒有Token可拿了就阻塞或者拒絕服務.redis
令牌桶的另一個好處是能夠方便的改變速度. 一旦須要提升速率,則按需提升放入桶中的令牌的速率. 通常會定時(好比100毫秒)往桶中增長必定數量的令牌, 有些變種算法則實時的計算應該增長的令牌的數量.算法
3、基於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的樂觀鎖機制sql
4、Redis樂觀鎖介紹
redis對事務的支持比較簡單。redis只能保證一個客戶端發起的事務命令能夠執行,中間不會插入其餘事務。由於redis是單線程的,因此作到上面這點很容易。通常redis接受到客戶端的命令後會當即執行,可是若是客戶端發起multi命令,redis不會當即執行,而是讓當前鏈接進入事務上下文,把命令放到隊列中,接受到exec命令後,redis會順序執行隊列中的命令。並把執行結果打包到一塊兒返回客戶端,以後就結束了事務上下文shell
1、簡單的事務控制數據庫
這個例子能夠看到:兩個set命令發出後並無當即執行而是放到隊列中,redis接受到exec命令纔開始執行。
若是有兩個線程同時修改了一個變量的值,如何控制事務回滾?下面看樂觀鎖怎麼控制的?json
2、樂觀鎖控制事務
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。
3、redis事務存在的問題
redis保證事務中的命令連續執行,可是若是其中一條命令執行失敗,事務並不回滾。
爲age +1的命令成功,由於anme是string類型的,因此不能作加操做,命令有一個失敗也不會回滾,age的值已經被修改了。
以上內容但願幫助到你們,不少PHPer在進階的時候總會遇到一些問題和瓶頸,業務代碼寫多了沒有方向感,不知道該從那裏入手去提高,對此我整理了一些資料,包括但不限於:分佈式架構、高可擴展、高性能、高併發、服務器性能調優、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql優化、shell腳本、Docker、微服務、Nginx等多個知識點高級進階乾貨須要的能夠免費分享給你們,須要戳這裏PHP進階架構師>>>視頻、面試文檔免費獲取