前言
起源:通常在產(chǎn)品的運行過程,我們可能會做數(shù)據(jù)埋點,以此來知道用戶觸發(fā)的行為,訪問了多少頁面,做了哪些操作,來方便產(chǎn)品根據(jù)用戶喜好的做不同的調(diào)整和推薦,同樣在服務端開發(fā)層面,也要做好“數(shù)據(jù)埋點”,去記錄接口的響應時長、接口調(diào)用頻率,參數(shù)頻率等,方便我們從后端角度去分析和優(yōu)化問題,如果遇到異常行為或者大量攻擊來源,我們可以具體針對到某個接口去進行優(yōu)化。
項目環(huán)境:
- framework:laravel 5.8+
- cache : redis >= 2.6.0
目前項目中幾乎都使用的是 graphql 接口,采用的 package 是 php lighthouse graphql,那么主要的場景就是去統(tǒng)計好,graphql 接口的請求次數(shù)即可。
實現(xiàn)GraphQL Record Middleware
首先建立一個middleware 用于稍后記錄接口的請求頻率,在這里可以使用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 配置中,或后添加到項目中 app/Http/Kernel.php 中,設置為全局中間件
'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 之后,開始就通過在Redis 來實現(xiàn)一個接口計數(shù)器。
添加接口計數(shù)器
首先要設置我們需要記錄的時間,如5秒,60秒,半小時、一個小時、5個小時、24小時等,用一個數(shù)組來實現(xiàn),具體可以根據(jù)自我需求來調(diào)整。
const PRECISION = [5, 60, 1800, 3600, 86400];
然后就開始添加對接口計數(shù)的邏輯,計數(shù)完成后,我們將其添加到zsset中,方便后續(xù)進行數(shù)據(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) {
//計算時間片
$pnow = intval($now / $prec) * $prec;
//生成一個hash key標識
$hash = "request:counter:{$prec}:$opName";
//增長接口請求數(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報告
app('sentry')->captureException($ex);
}
return $redis;
}
然后請求一下接口,用medis查看一下數(shù)據(jù)。


查詢、分析數(shù)據(jù)
數(shù)據(jù)記錄完善后,可以通過opName 及 prec兩個屬性來查詢,如查詢24小時的tag接口訪問數(shù)據(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小時的訪問統(tǒng)計
$data = $this->getRequestCounter('tagQuery', '86400');
清除數(shù)據(jù)
完善一系列步驟后,我們可能需要將過期和一些不必要的數(shù)據(jù)進行清理,可以通過定時任務來進行定期清理,相關(guān)實現(xiàn)如下:
/**
* 清理請求計數(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++;
//當前hash存在
if ($hash) {
$hash = $hash[0];
//計算刪除截止時間
$cutoff = intval(microtime(true) - ($clearDay * 24 * 60 * 60));
//優(yōu)先刪除時間較遠的數(shù)據(jù)
$samples = array_map('intval', $redis->hkeys($hash));
sort($samples);
//需要刪除的數(shù)據(jù)
$removes = array_filter($samples, function ($item) use ($cutoff) {
return $item = $cutoff;
});
if (count($removes)) {
$redis->hdel($hash, ...$removes);
//如果整個數(shù)據(jù)都過期了的話,就清除掉統(tǒng)計的數(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('清理完成');
}
}
清理一個30天前的數(shù)據(jù):
$this->clearRequestCounter(30);
整合代碼
我們將所有操作接口統(tǒng)計的代碼,單獨封裝到一個類中,然后對外提供靜態(tài)函數(shù)調(diào)用,既實現(xiàn)了職責單一,又方便集成到其他不同的模塊使用。
?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';
/**
* 更新請求計數(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) {
//計算時間片
$pnow = intval($now / $prec) * $prec;
//生成一個hash key標識
$hash = self::counterCacheKey($opName, $prec);
//增長接口請求數(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報告
app('sentry')->captureException($ex);
}
return $redis;
}
/**
* 獲取接口訪問計數(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;
}
/**
* 清理請求計數(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++;
//當前hash存在
if ($hash) {
$hash = $hash[0];
//計算刪除截止時間
$cutoff = intval(microtime(true) - ($clearDay * 24 * 60 * 60));
//優(yōu)先刪除時間較遠的數(shù)據(jù)
$samples = array_map('intval', $redis->hkeys($hash));
sort($samples);
//需要刪除的數(shù)據(jù)
$removes = array_filter($samples, function ($item) use ($cutoff) {
return $item = $cutoff;
});
if (count($removes)) {
$redis->hdel($hash, ...$removes);
//如果整個數(shù)據(jù)都過期了的話,就清除掉統(tǒng)計的數(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é)尾
上訴代碼就實現(xiàn)了基于GraphQL的請求頻率記錄,但是使用不止適用于GraphQL接口,也可以基于Rest接口、模塊計數(shù)等統(tǒng)計行為,只要有唯一的operation name即可。
到此這篇關(guān)于Laravel中GraphQL接口請求頻率的文章就介紹到這了,更多相關(guān)Laravel中GraphQL接口請求頻率內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!