设为首页收藏本站
网站公告 | 这是第一条公告
     

 找回密码
 立即注册
缓存时间15 现在时间15 缓存数据 一个人挺好的

一个人挺好的 -- 一个

查看: 1328|回复: 0

Laravel中GraphQL接口请求频率实战记录

[复制链接]

  离线 

TA的专栏

  • 打卡等级:热心大叔
  • 打卡总天数:224
  • 打卡月天数:0
  • 打卡总奖励:3964
  • 最近打卡:2025-04-12 11:50:01
等级头衔

等級:晓枫资讯-上等兵

在线时间
5 小时

积分成就
威望
0
贡献
454
主题
390
精华
0
金钱
5222
积分
890
注册时间
2023-1-3
最后登录
2025-5-31

发表于 2023-2-12 20:45:16 | 显示全部楼层 |阅读模式
前言
起源:通常在产品的运行过程,我们可能会做数据埋点,以此来知道用户触发的行为,访问了多少页面,做了哪些操作,来方便产品根据用户喜好的做不同的调整和推荐,同样在服务端开发层面,也要做好“数据埋点”,去记录接口的响应时长、接口调用频率,参数频率等,方便我们从后端角度去分析和优化问题,如果遇到异常行为或者大量攻击来源,我们可以具体针对到某个接口去进行优化。
项目环境:
      
  • framework:laravel 5.8+  
  • cache : redis >= 2.6.0
目前项目中几乎都使用的是 graphql 接口,采用的 package 是 php lighthouse graphql,那么主要的场景就是去统计好,graphql 接口的请求次数即可。
实现GraphQL Record Middleware
首先建立一个middleware 用于稍后记录接口的请求频率,在这里可以使用artisan 脚手架快速创建:
  1. php artisan make:middleware GraphQLRecord
复制代码
  1. <?php

  2. namespace App\Http\Middleware;

  3. use Closure;

  4. class GraphQLRecord
  5. {
  6.   /**
  7.    * Handle an incoming request.
  8.    *
  9.    * @param \Illuminate\Http\Request $request
  10.    * @param \Closure $next
  11.    * @return mixed
  12.    */
  13.   public function handle($request, Closure $next)
  14.   {
  15.     return $next($request);
  16.   }
  17. }
复制代码
然后添加到 app/config/lighthouse.php middleware 配置中,或后添加到项目中 app/Http/Kernel.php 中,设置为全局中间件
  1. 'middleware' => [
  2.   \App\Http\Middleware\GraphQLRecord::class,
  3.   \Nuwave\Lighthouse\Support\Http\Middleware\AcceptJson::class,
  4. ],
复制代码
获取 GraphQL Operation Name
  1. public function handle($request, Closure $next)
  2. {
  3.     $opName = $request->get('operationName');
  4.     return $next($request);
  5. }
复制代码
获取到 Operation Name 之后,开始就通过在Redis 来实现一个接口计数器。
添加接口计数器
首先要设置我们需要记录的时间,如5秒,60秒,半小时、一个小时、5个小时、24小时等,用一个数组来实现,具体可以根据自我需求来调整。
  1. const PRECISION = [5, 60, 1800, 3600, 86400];
复制代码
然后就开始添加对接口计数的逻辑,计数完成后,我们将其添加到zsset中,方便后续进行数据查询等操作。
  1.   /**
  2.    * 更新请求计数器
  3.    *
  4.    * @param string $opName
  5.    * @param integer $count
  6.    * @return void
  7.    */
  8.   public function updateRequestCounter(string $opName, $count = 1)
  9.   {
  10.     $now  = microtime(true);
  11.     $redis = self::getRedisConn();
  12.     if ($redis) {
  13.       $pipe = $redis->pipeline();
  14.       foreach (self::PRECISION as $prec) {
  15.         //计算时间片
  16.         $pnow = intval($now / $prec) * $prec;
  17.         //生成一个hash key标识
  18.         $hash = "request:counter:{$prec}:$opName";
  19.         //增长接口请求数
  20.         $pipe->hincrby($hash, $pnow, 1);
  21.         // 添加到集合中,方便后续数据查询
  22.         $pipe->zadd('request:counter', [$hash => 0]);
  23.       }
  24.       $pipe->execute();
  25.     }
  26.   }

  27.   /**
  28.    * 获取Redis连接
  29.    *
  30.    * @return object
  31.    */
  32.   public static function getRedisConn()
  33.   {
  34.     $redis = Redis::connection('cache');
  35.     try {
  36.       $redis->ping();
  37.     } catch (Exception $ex) {
  38.       $redis = null;
  39.       //丢给sentry报告
  40.       app('sentry')->captureException($ex);
  41.     }

  42.     return $redis;
  43.   }
复制代码
然后请求一下接口,用medis查看一下数据。
214616mzz9zy3r3y3ru9bb.jpg

214617so1okzqn4ddgcn6d.jpg

查询、分析数据
数据记录完善后,可以通过opName 及 prec两个属性来查询,如查询24小时的tag接口访问数据
  1.   /**
  2.    * 获取接口访问计数
  3.    *
  4.    * @param string $opName
  5.    * @param integer $prec
  6.    * @return array
  7.    */
  8.   public static function getRequestCounter(string $opName, int $prec)
  9.   {
  10.     $data = [];
  11.     $redis = self::getRedisConn();
  12.     if ($redis) {
  13.       $hash   = "request:counter:{$prec}:$opName";
  14.       $hashData = $redis->hgetall($hash);
  15.       foreach ($hashData as $k => $v) {
  16.         $date  = date("Y/m/d", $k);
  17.         $data[] = ['timestamp' => $k, 'value' => $v, 'date' => $date];
  18.       }
  19.     }

  20.     return $data;
  21.   }
复制代码
获取 tag 接口 24小时的访问统计
  1. $data = $this->getRequestCounter('tagQuery', '86400');
复制代码
清除数据
完善一系列步骤后,我们可能需要将过期和一些不必要的数据进行清理,可以通过定时任务来进行定期清理,相关实现如下:
  1. /**
  2.    * 清理请求计数
  3.    *
  4.    * @param integer $clearDay
  5.    * @return void
  6.    */
  7.   public function clearRequestCounter($clearDay = 7)
  8.   {
  9.     $index   = 0;
  10.     $startTime = microtime(true);
  11.     $redis   = self::getRedisConn();
  12.     if ($redis) {
  13.       //可以清理的情况下
  14.       while ($index < $redis->zcard('request:counter')) {
  15.         $hash = $redis->zrange('request:counter', $index, $index);
  16.         $index++;

  17.         //当前hash存在
  18.         if ($hash) {
  19.           $hash = $hash[0];
  20.           //计算删除截止时间
  21.           $cutoff = intval(microtime(true) - ($clearDay * 24 * 60 * 60));

  22.           //优先删除时间较远的数据
  23.           $samples = array_map('intval', $redis->hkeys($hash));
  24.           sort($samples);

  25.           //需要删除的数据
  26.           $removes = array_filter($samples, function ($item) use (&$cutoff) {
  27.             return $item <= $cutoff;
  28.           });
  29.           if (count($removes)) {
  30.             $redis->hdel($hash, ...$removes);
  31.             //如果整个数据都过期了的话,就清除掉统计的数据
  32.             if (count($removes) == count($samples)) {
  33.               $trans = $redis->transaction(['cas' => true]);
  34.               try {
  35.                 $trans->watch($hash);
  36.                 if (!$trans->hlen($hash)) {
  37.                   $trans->multi();
  38.                   $trans->zrem('request:counter', $hash);
  39.                   $trans->execute();
  40.                   $index--;
  41.                 } else {
  42.                   $trans->unwatch();
  43.                 }
  44.               } catch (\Exception $ex) {
  45.                 dump($ex);
  46.               }
  47.             }
  48.           }

  49.         }
  50.       }
  51.       dump('清理完成');
  52.     }

  53.   }
复制代码
清理一个30天前的数据:
  1. $this->clearRequestCounter(30);
复制代码
整合代码

我们将所有操作接口统计的代码,单独封装到一个类中,然后对外提供静态函数调用,既实现了职责单一,又方便集成到其他不同的模块使用。
  1. <?php
  2. namespace App\Helpers;

  3. use Illuminate\Support\Facades\Redis;

  4. class RequestCounter
  5. {
  6.   const PRECISION = [5, 60, 1800, 3600, 86400];

  7.   const REQUEST_COUNTER_CACHE_KEY = 'request:counter';

  8.   /**
  9.    * 更新请求计数器
  10.    *
  11.    * @param string $opName
  12.    * @param integer $count
  13.    * @return void
  14.    */
  15.   public static function updateRequestCounter(string $opName, $count = 1)
  16.   {
  17.     $now  = microtime(true);
  18.     $redis = self::getRedisConn();
  19.     if ($redis) {
  20.       $pipe = $redis->pipeline();
  21.       foreach (self::PRECISION as $prec) {
  22.         //计算时间片
  23.         $pnow = intval($now / $prec) * $prec;
  24.         //生成一个hash key标识
  25.         $hash = self::counterCacheKey($opName, $prec);
  26.         //增长接口请求数
  27.         $pipe->hincrby($hash, $pnow, 1);
  28.         // 添加到集合中,方便后续数据查询
  29.         $pipe->zadd(self::REQUEST_COUNTER_CACHE_KEY, [$hash => 0]);
  30.       }
  31.       $pipe->execute();
  32.     }
  33.   }

  34.   /**
  35.    * 获取Redis连接
  36.    *
  37.    * @return object
  38.    */
  39.   public static function getRedisConn()
  40.   {
  41.     $redis = Redis::connection('cache');
  42.     try {
  43.       $redis->ping();
  44.     } catch (Exception $ex) {
  45.       $redis = null;
  46.       //丢给sentry报告
  47.       app('sentry')->captureException($ex);
  48.     }

  49.     return $redis;
  50.   }

  51.   /**
  52.    * 获取接口访问计数
  53.    *
  54.    * @param string $opName
  55.    * @param integer $prec
  56.    * @return array
  57.    */
  58.   public static function getRequestCounter(string $opName, int $prec)
  59.   {
  60.     $data = [];
  61.     $redis = self::getRedisConn();
  62.     if ($redis) {
  63.       $hash   = self::counterCacheKey($opName, $prec);
  64.       $hashData = $redis->hgetall($hash);
  65.       foreach ($hashData as $k => $v) {
  66.         $date  = date("Y/m/d", $k);
  67.         $data[] = ['timestamp' => $k, 'value' => $v, 'date' => $date];
  68.       }
  69.     }

  70.     return $data;
  71.   }

  72.   /**
  73.    * 清理请求计数
  74.    *
  75.    * @param integer $clearDay
  76.    * @return void
  77.    */
  78.   public static function clearRequestCounter($clearDay = 7)
  79.   {
  80.     $index   = 0;
  81.     $startTime = microtime(true);
  82.     $redis   = self::getRedisConn();
  83.     if ($redis) {
  84.       //可以清理的情况下
  85.       while ($index < $redis->zcard(self::REQUEST_COUNTER_CACHE_KEY)) {
  86.         $hash = $redis->zrange(self::REQUEST_COUNTER_CACHE_KEY, $index, $index);
  87.         $index++;

  88.         //当前hash存在
  89.         if ($hash) {
  90.           $hash = $hash[0];
  91.           //计算删除截止时间
  92.           $cutoff = intval(microtime(true) - ($clearDay * 24 * 60 * 60));

  93.           //优先删除时间较远的数据
  94.           $samples = array_map('intval', $redis->hkeys($hash));
  95.           sort($samples);

  96.           //需要删除的数据
  97.           $removes = array_filter($samples, function ($item) use (&$cutoff) {
  98.             return $item <= $cutoff;
  99.           });
  100.           if (count($removes)) {
  101.             $redis->hdel($hash, ...$removes);
  102.             //如果整个数据都过期了的话,就清除掉统计的数据
  103.             if (count($removes) == count($samples)) {
  104.               $trans = $redis->transaction(['cas' => true]);
  105.               try {
  106.                 $trans->watch($hash);
  107.                 if (!$trans->hlen($hash)) {
  108.                   $trans->multi();
  109.                   $trans->zrem(self::REQUEST_COUNTER_CACHE_KEY, $hash);
  110.                   $trans->execute();
  111.                   $index--;
  112.                 } else {
  113.                   $trans->unwatch();
  114.                 }
  115.               } catch (\Exception $ex) {
  116.                 dump($ex);
  117.               }
  118.             }
  119.           }

  120.         }
  121.       }
  122.       dump('清理完成');
  123.     }

  124.   }

  125.   public static function counterCacheKey($opName, $prec)
  126.   {
  127.     $key = "request:counter:{$prec}:$opName";

  128.     return $key;
  129.   }
  130. }
复制代码
在Middleware中使用.
  1. <?php

  2. namespace App\Http\Middleware;

  3. use App\Helpers\RequestCounter;
  4. use Closure;

  5. class GraphQLRecord
  6. {

  7.   /**
  8.    * Handle an incoming request.
  9.    *
  10.    * @param \Illuminate\Http\Request $request
  11.    * @param \Closure $next
  12.    * @return mixed
  13.    */
  14.   public function handle($request, Closure $next)
  15.   {
  16.     $opName = $request->get('operationName');
  17.     if (!empty($opName)) {
  18.       RequestCounter::updateRequestCounter($opName);
  19.     }

  20.     return $next($request);
  21.   }
  22. }
复制代码
结尾
上诉代码就实现了基于GraphQL的请求频率记录,但是使用不止适用于GraphQL接口,也可以基于Rest接口、模块计数等统计行为,只要有唯一的operation name即可。
到此这篇关于Laravel中GraphQL接口请求频率的文章就介绍到这了,更多相关Laravel中GraphQL接口请求频率内容请搜索晓枫资讯以前的文章或继续浏览下面的相关文章希望大家以后多多支持晓枫资讯!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
      1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
      2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
      3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:点击这里给我发消息进行删除处理。
      4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
      5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~
严禁发布广告,淫秽、色情、赌博、暴力、凶杀、恐怖、间谍及其他违反国家法律法规的内容。!晓枫资讯-社区
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|晓枫资讯--科技资讯社区 本站已运行

CopyRight © 2022-2025 晓枫资讯--科技资讯社区 ( BBS.yzwlo.com ) . All Rights Reserved .

晓枫资讯--科技资讯社区

本站内容由用户自主分享和转载自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

如有侵权、违反国家法律政策行为,请联系我们,我们会第一时间及时清除和处理! 举报反馈邮箱:点击这里给我发消息

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表