前言
起源:通常在产品的运行过程,我们可能会做数据埋点,以此来知道用户触发的行为,访问了多少页面,做了哪些操作,来方便产品根据用户喜好的做不同的调整和推荐,同样在服务端开发层面,也要做好“数据埋点”,去记录接口的响应时长、接口调用频率,参数频率等,方便我们从后端角度去分析和优化问题,如果遇到异常行为或者大量攻击来源,我们可以具体针对到某个接口去进行优化。
项目环境:
- framework:laravel 5.8+
- cache : redis >= 2.6.0
目前项目中几乎都使用的是 graphql 接口,采用的 package 是 php lighthouse graphql,那么主要的场景就是去统计好,graphql 接口的请求次数即可。
实现GraphQL Record Middleware
首先建立一个middleware 用于稍后记录接口的请求频率,在这里可以使用artisan 脚手架快速创建:
php artisan make:middleware GraphQLRecord
<"htmlcode">'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 来实现一个接口计数器。
添加接口计数器
首先要设置我们需要记录的时间,如5秒,60秒,半小时、一个小时、5个小时、24小时等,用一个数组来实现,具体可以根据自我需求来调整。
const PRECISION = [5, 60, 1800, 3600, 86400];然后就开始添加对接口计数的逻辑,计数完成后,我们将其添加到zsset中,方便后续进行数据查询等操作。
/** * 更新请求计数器 * * @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"; //增长接口请求数 $pipe->hincrby($hash, $pnow, 1); // 添加到集合中,方便后续数据查询 $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查看一下数据。
查询、分析数据
数据记录完善后,可以通过opName 及 prec两个属性来查询,如查询24小时的tag接口访问数据
/** * 获取接口访问计数 * * @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小时的访问统计
$data = $this->getRequestCounter('tagQuery', '86400');清除数据
完善一系列步骤后,我们可能需要将过期和一些不必要的数据进行清理,可以通过定时任务来进行定期清理,相关实现如下:
/** * 清理请求计数 * * @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)); //优先删除时间较远的数据 $samples = array_map('intval', $redis->hkeys($hash)); sort($samples); //需要删除的数据 $removes = array_filter($samples, function ($item) use (&$cutoff) { return $item <= $cutoff; }); if (count($removes)) { $redis->hdel($hash, ...$removes); //如果整个数据都过期了的话,就清除掉统计的数据 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天前的数据:
$this->clearRequestCounter(30);整合代码
我们将所有操作接口统计的代码,单独封装到一个类中,然后对外提供静态函数调用,既实现了职责单一,又方便集成到其他不同的模块使用。
<"Y/m/d", $k); $data[] = ['timestamp' => $k, 'value' => $v, 'date' => $date]; } } return $data; } /** * 清理请求计数 * * @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)); //优先删除时间较远的数据 $samples = array_map('intval', $redis->hkeys($hash)); sort($samples); //需要删除的数据 $removes = array_filter($samples, function ($item) use (&$cutoff) { return $item <= $cutoff; }); if (count($removes)) { $redis->hdel($hash, ...$removes); //如果整个数据都过期了的话,就清除掉统计的数据 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中使用.
<"color: #ff0000">结尾上诉代码就实现了基于GraphQL的请求频率记录,但是使用不止适用于GraphQL接口,也可以基于Rest接口、模块计数等统计行为,只要有唯一的operation name即可。
免责声明:本站文章均来自网站采集或用户投稿,网站不提供任何软件下载或自行开发的软件! 如有用户或公司发现本站内容信息存在侵权行为,请邮件告知! 858582#qq.com
《魔兽世界》大逃杀!60人新游玩模式《强袭风暴》3月21日上线
暴雪近日发布了《魔兽世界》10.2.6 更新内容,新游玩模式《强袭风暴》即将于3月21 日在亚服上线,届时玩家将前往阿拉希高地展开一场 60 人大逃杀对战。
艾泽拉斯的冒险者已经征服了艾泽拉斯的大地及遥远的彼岸。他们在对抗世界上最致命的敌人时展现出过人的手腕,并且成功阻止终结宇宙等级的威胁。当他们在为即将于《魔兽世界》资料片《地心之战》中来袭的萨拉塔斯势力做战斗准备时,他们还需要在熟悉的阿拉希高地面对一个全新的敌人──那就是彼此。在《巨龙崛起》10.2.6 更新的《强袭风暴》中,玩家将会进入一个全新的海盗主题大逃杀式限时活动,其中包含极高的风险和史诗级的奖励。
《强袭风暴》不是普通的战场,作为一个独立于主游戏之外的活动,玩家可以用大逃杀的风格来体验《魔兽世界》,不分职业、不分装备(除了你在赛局中捡到的),光是技巧和战略的强弱之分就能决定出谁才是能坚持到最后的赢家。本次活动将会开放单人和双人模式,玩家在加入海盗主题的预赛大厅区域前,可以从强袭风暴角色画面新增好友。游玩游戏将可以累计名望轨迹,《巨龙崛起》和《魔兽世界:巫妖王之怒 经典版》的玩家都可以获得奖励。
更新日志
- 群星《继续微笑致敬许冠杰》[低速原抓WAV+CUE]
- 潘秀琼.2003-国语难忘金曲珍藏集【皇星全音】【WAV+CUE】
- 林东松.1997-2039玫瑰事件【宝丽金】【WAV+CUE】
- 谭咏麟.2022-倾·听【环球】【WAV+CUE】
- 4complete《丛生》[320K/MP3][85.26MB]
- 4complete《丛生》[FLAC/分轨][218.01MB]
- 羽泉《给未来的你&天黑天亮》[WAV+CUE][968M]
- 庄心妍《我也许在等候》[低速原抓WAV+CUE]
- 王雅洁《小调歌后2》[原抓WAV+CUE]
- 中国武警男声合唱团《辉煌之声1天路》[DTS-WAV分轨]
- 紫薇《旧曲新韵》[320K/MP3][175.29MB]
- 紫薇《旧曲新韵》[FLAC/分轨][550.18MB]
- 周深《反深代词》[先听版][320K/MP3][72.71MB]
- 李佳薇.2024-会发光的【黑籁音乐】【FLAC分轨】
- 后弦.2012-很有爱【天浩盛世】【WAV+CUE】