Laravel中GraphQL接口請(qǐng)求頻率實(shí)戰(zhàn)記錄
前言
起源:通常在產(chǎn)品的運(yùn)行過(guò)程,我們可能會(huì)做數(shù)據(jù)埋點(diǎn),以此來(lái)知道用戶觸發(fā)的行為,訪問(wèn)了多少頁(yè)面,做了哪些操作,來(lái)方便產(chǎn)品根據(jù)用戶喜好的做不同的調(diào)整和推薦,同樣在服務(wù)端開(kāi)發(fā)層面,也要做好“數(shù)據(jù)埋點(diǎn)”,去記錄接口的響應(yīng)時(shí)長(zhǎng)、接口調(diào)用頻率,參數(shù)頻率等,方便我們從后端角度去分析和優(yōu)化問(wèn)題,如果遇到異常行為或者大量攻擊來(lái)源,我們可以具體針對(duì)到某個(gè)接口去進(jìn)行優(yōu)化。
項(xiàng)目環(huán)境:
- framework:laravel 5.8+
- cache : redis >= 2.6.0
目前項(xiàng)目中幾乎都使用的是 graphql 接口,采用的 package 是 php lighthouse graphql,那么主要的場(chǎng)景就是去統(tǒng)計(jì)好,graphql 接口的請(qǐng)求次數(shù)即可。
實(shí)現(xiàn)GraphQL Record Middleware
首先建立一個(gè)middleware 用于稍后記錄接口的請(qǐng)求頻率,在這里可以使用artisan 腳手架快速創(chuàng)建:
php artisan make:middleware GraphQLRecord
<?php namespace App\Http\Middleware; use Closure; class GraphQLRecord { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { return $next($request); } }
然后添加到 app/config/lighthouse.php middleware 配置中,或后添加到項(xiàng)目中 app/Http/Kernel.php 中,設(shè)置為全局中間件
'middleware' => [ \App\Http\Middleware\GraphQLRecord::class, \Nuwave\Lighthouse\Support\Http\Middleware\AcceptJson::class, ],
獲取 GraphQL Operation Name
public function handle($request, Closure $next) { $opName = $request->get('operationName'); return $next($request); }
獲取到 Operation Name 之后,開(kāi)始就通過(guò)在Redis 來(lái)實(shí)現(xiàn)一個(gè)接口計(jì)數(shù)器。
添加接口計(jì)數(shù)器
首先要設(shè)置我們需要記錄的時(shí)間,如5秒,60秒,半小時(shí)、一個(gè)小時(shí)、5個(gè)小時(shí)、24小時(shí)等,用一個(gè)數(shù)組來(lái)實(shí)現(xiàn),具體可以根據(jù)自我需求來(lái)調(diào)整。
const PRECISION = [5, 60, 1800, 3600, 86400];
然后就開(kāi)始添加對(duì)接口計(jì)數(shù)的邏輯,計(jì)數(shù)完成后,我們將其添加到zsset中,方便后續(xù)進(jìn)行數(shù)據(jù)查詢等操作。
/** * 更新請(qǐng)求計(jì)數(shù)器 * * @param string $opName * @param integer $count * @return void */ public function updateRequestCounter(string $opName, $count = 1) { $now = microtime(true); $redis = self::getRedisConn(); if ($redis) { $pipe = $redis->pipeline(); foreach (self::PRECISION as $prec) { //計(jì)算時(shí)間片 $pnow = intval($now / $prec) * $prec; //生成一個(gè)hash key標(biāo)識(shí) $hash = "request:counter:{$prec}:$opName"; //增長(zhǎng)接口請(qǐng)求數(shù) $pipe->hincrby($hash, $pnow, 1); // 添加到集合中,方便后續(xù)數(shù)據(jù)查詢 $pipe->zadd('request:counter', [$hash => 0]); } $pipe->execute(); } } /** * 獲取Redis連接 * * @return object */ public static function getRedisConn() { $redis = Redis::connection('cache'); try { $redis->ping(); } catch (Exception $ex) { $redis = null; //丟給sentry報(bào)告 app('sentry')->captureException($ex); } return $redis; }
然后請(qǐng)求一下接口,用medis查看一下數(shù)據(jù)。
查詢、分析數(shù)據(jù)
數(shù)據(jù)記錄完善后,可以通過(guò)opName 及 prec兩個(gè)屬性來(lái)查詢,如查詢24小時(shí)的tag接口訪問(wèn)數(shù)據(jù)
/** * 獲取接口訪問(wèn)計(jì)數(shù) * * @param string $opName * @param integer $prec * @return array */ public static function getRequestCounter(string $opName, int $prec) { $data = []; $redis = self::getRedisConn(); if ($redis) { $hash = "request:counter:{$prec}:$opName"; $hashData = $redis->hgetall($hash); foreach ($hashData as $k => $v) { $date = date("Y/m/d", $k); $data[] = ['timestamp' => $k, 'value' => $v, 'date' => $date]; } } return $data; }
獲取 tag 接口 24小時(shí)的訪問(wèn)統(tǒng)計(jì)
$data = $this->getRequestCounter('tagQuery', '86400');
清除數(shù)據(jù)
完善一系列步驟后,我們可能需要將過(guò)期和一些不必要的數(shù)據(jù)進(jìn)行清理,可以通過(guò)定時(shí)任務(wù)來(lái)進(jìn)行定期清理,相關(guān)實(shí)現(xiàn)如下:
/** * 清理請(qǐng)求計(jì)數(shù) * * @param integer $clearDay * @return void */ public function clearRequestCounter($clearDay = 7) { $index = 0; $startTime = microtime(true); $redis = self::getRedisConn(); if ($redis) { //可以清理的情況下 while ($index < $redis->zcard('request:counter')) { $hash = $redis->zrange('request:counter', $index, $index); $index++; //當(dāng)前hash存在 if ($hash) { $hash = $hash[0]; //計(jì)算刪除截止時(shí)間 $cutoff = intval(microtime(true) - ($clearDay * 24 * 60 * 60)); //優(yōu)先刪除時(shí)間較遠(yuǎn)的數(shù)據(jù) $samples = array_map('intval', $redis->hkeys($hash)); sort($samples); //需要?jiǎng)h除的數(shù)據(jù) $removes = array_filter($samples, function ($item) use (&$cutoff) { return $item <= $cutoff; }); if (count($removes)) { $redis->hdel($hash, ...$removes); //如果整個(gè)數(shù)據(jù)都過(guò)期了的話,就清除掉統(tǒng)計(jì)的數(shù)據(jù) if (count($removes) == count($samples)) { $trans = $redis->transaction(['cas' => true]); try { $trans->watch($hash); if (!$trans->hlen($hash)) { $trans->multi(); $trans->zrem('request:counter', $hash); $trans->execute(); $index--; } else { $trans->unwatch(); } } catch (\Exception $ex) { dump($ex); } } } } } dump('清理完成'); } }
清理一個(gè)30天前的數(shù)據(jù):
$this->clearRequestCounter(30);
整合代碼
我們將所有操作接口統(tǒng)計(jì)的代碼,單獨(dú)封裝到一個(gè)類中,然后對(duì)外提供靜態(tài)函數(shù)調(diào)用,既實(shí)現(xiàn)了職責(zé)單一,又方便集成到其他不同的模塊使用。
<?php namespace App\Helpers; use Illuminate\Support\Facades\Redis; class RequestCounter { const PRECISION = [5, 60, 1800, 3600, 86400]; const REQUEST_COUNTER_CACHE_KEY = 'request:counter'; /** * 更新請(qǐng)求計(jì)數(shù)器 * * @param string $opName * @param integer $count * @return void */ public static function updateRequestCounter(string $opName, $count = 1) { $now = microtime(true); $redis = self::getRedisConn(); if ($redis) { $pipe = $redis->pipeline(); foreach (self::PRECISION as $prec) { //計(jì)算時(shí)間片 $pnow = intval($now / $prec) * $prec; //生成一個(gè)hash key標(biāo)識(shí) $hash = self::counterCacheKey($opName, $prec); //增長(zhǎng)接口請(qǐng)求數(shù) $pipe->hincrby($hash, $pnow, 1); // 添加到集合中,方便后續(xù)數(shù)據(jù)查詢 $pipe->zadd(self::REQUEST_COUNTER_CACHE_KEY, [$hash => 0]); } $pipe->execute(); } } /** * 獲取Redis連接 * * @return object */ public static function getRedisConn() { $redis = Redis::connection('cache'); try { $redis->ping(); } catch (Exception $ex) { $redis = null; //丟給sentry報(bào)告 app('sentry')->captureException($ex); } return $redis; } /** * 獲取接口訪問(wèn)計(jì)數(shù) * * @param string $opName * @param integer $prec * @return array */ public static function getRequestCounter(string $opName, int $prec) { $data = []; $redis = self::getRedisConn(); if ($redis) { $hash = self::counterCacheKey($opName, $prec); $hashData = $redis->hgetall($hash); foreach ($hashData as $k => $v) { $date = date("Y/m/d", $k); $data[] = ['timestamp' => $k, 'value' => $v, 'date' => $date]; } } return $data; } /** * 清理請(qǐng)求計(jì)數(shù) * * @param integer $clearDay * @return void */ public static function clearRequestCounter($clearDay = 7) { $index = 0; $startTime = microtime(true); $redis = self::getRedisConn(); if ($redis) { //可以清理的情況下 while ($index < $redis->zcard(self::REQUEST_COUNTER_CACHE_KEY)) { $hash = $redis->zrange(self::REQUEST_COUNTER_CACHE_KEY, $index, $index); $index++; //當(dāng)前hash存在 if ($hash) { $hash = $hash[0]; //計(jì)算刪除截止時(shí)間 $cutoff = intval(microtime(true) - ($clearDay * 24 * 60 * 60)); //優(yōu)先刪除時(shí)間較遠(yuǎn)的數(shù)據(jù) $samples = array_map('intval', $redis->hkeys($hash)); sort($samples); //需要?jiǎng)h除的數(shù)據(jù) $removes = array_filter($samples, function ($item) use (&$cutoff) { return $item <= $cutoff; }); if (count($removes)) { $redis->hdel($hash, ...$removes); //如果整個(gè)數(shù)據(jù)都過(guò)期了的話,就清除掉統(tǒng)計(jì)的數(shù)據(jù) if (count($removes) == count($samples)) { $trans = $redis->transaction(['cas' => true]); try { $trans->watch($hash); if (!$trans->hlen($hash)) { $trans->multi(); $trans->zrem(self::REQUEST_COUNTER_CACHE_KEY, $hash); $trans->execute(); $index--; } else { $trans->unwatch(); } } catch (\Exception $ex) { dump($ex); } } } } } dump('清理完成'); } } public static function counterCacheKey($opName, $prec) { $key = "request:counter:{$prec}:$opName"; return $key; } }
在Middleware中使用.
<?php namespace App\Http\Middleware; use App\Helpers\RequestCounter; use Closure; class GraphQLRecord { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { $opName = $request->get('operationName'); if (!empty($opName)) { RequestCounter::updateRequestCounter($opName); } return $next($request); } }
結(jié)尾
上訴代碼就實(shí)現(xiàn)了基于GraphQL的請(qǐng)求頻率記錄,但是使用不止適用于GraphQL接口,也可以基于Rest接口、模塊計(jì)數(shù)等統(tǒng)計(jì)行為,只要有唯一的operation name即可。
到此這篇關(guān)于Laravel中GraphQL接口請(qǐng)求頻率的文章就介紹到這了,更多相關(guān)Laravel中GraphQL接口請(qǐng)求頻率內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
php使用curl和正則表達(dá)式抓取網(wǎng)頁(yè)數(shù)據(jù)示例
這篇文章主要介紹了php使用curl和正則表達(dá)式抓取網(wǎng)頁(yè)數(shù)據(jù)示例,這里是抓取某網(wǎng)站的小說(shuō),需要的朋友可以修改一下抓取其它數(shù)據(jù)2014-04-04PHP設(shè)計(jì)模式之工廠模式(Factory)入門(mén)與應(yīng)用詳解
這篇文章主要介紹了PHP設(shè)計(jì)模式之工廠模式(Factory),結(jié)合實(shí)例形式詳細(xì)分析了PHP工廠模式的概念、原理、基本應(yīng)用與相關(guān)操作注意事項(xiàng),需要的朋友可以參考下2019-12-12ThinkPHP控制器間實(shí)現(xiàn)相互調(diào)用的方法
這篇文章主要介紹了ThinkPHP控制器間實(shí)現(xiàn)相互調(diào)用的方法,主要通過(guò)A()方法實(shí)現(xiàn)這一功能,可以有效的提高代碼的重復(fù)利用率,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2014-10-10PHP的foreach中使用引用時(shí)需要注意的一個(gè)問(wèn)題和解決方法
這篇文章主要介紹了PHP的foreach中使用引用時(shí)需要注意的一個(gè)問(wèn)題和解決方法,即數(shù)組最后一個(gè)元素的值會(huì)發(fā)生改變的情況,需要的朋友可以參考下2014-05-05php curl模擬post請(qǐng)求小實(shí)例
使用php curl模擬post請(qǐng)求的小例子,提供大家學(xué)習(xí)一下2013-11-11Laravel中表單size驗(yàn)證數(shù)字示例詳解
Laravel 的驗(yàn)證功能非常強(qiáng)大,基本上常見(jiàn)的需求都有對(duì)應(yīng)的驗(yàn)證規(guī)則,下面這篇文章主要給大家介紹了關(guān)于Laravel中表單size驗(yàn)證數(shù)字的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2018-07-07php分頁(yè)思路以及在Zend?Framework框架中的使用
php分頁(yè)思路以及在Zend?Framework框架中的使用,需要的朋友可以參考下2012-05-05