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


Posted in PHP onSeptember 01, 2020

前言

起源:通常在产品的运行过程,我们可能会做数据埋点,以此来知道用户触发的行为,访问了多少页面,做了哪些操作,来方便产品根据用户喜好的做不同的调整和推荐,同样在服务端开发层面,也要做好“数据埋点”,去记录接口的响应时长、接口调用频率,参数频率等,方便我们从后端角度去分析和优化问题,如果遇到异常行为或者大量攻击来源,我们可以具体针对到某个接口去进行优化。

项目环境:

  • 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
<?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 来实现一个接口计数器。

添加接口计数器

首先要设置我们需要记录的时间,如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查看一下数据。

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

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

查询、分析数据

数据记录完善后,可以通过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);

整合代码

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

<?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';

  /**
   * 更新请求计数器
   *
   * @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);
        //增长接口请求数
        $pipe->hincrby($hash, $pnow, 1);
        // 添加到集合中,方便后续数据查询
        $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;
  }

  /**
   * 获取接口访问计数
   *
   * @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;
  }

  /**
   * 清理请求计数
   *
   * @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中使用.

<?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);
  }
}

结尾

上诉代码就实现了基于GraphQL的请求频率记录,但是使用不止适用于GraphQL接口,也可以基于Rest接口、模块计数等统计行为,只要有唯一的operation name即可。

到此这篇关于Laravel中GraphQL接口请求频率的文章就介绍到这了,更多相关Laravel中GraphQL接口请求频率内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

PHP 相关文章推荐
台湾中原大学php教程孙仲岳主讲
Jan 07 PHP
遍历指定目录下的所有目录和文件的php代码
Nov 27 PHP
php发送post请求函数分享
Mar 06 PHP
Parse正式发布开源PHP SDK
Aug 11 PHP
thinkphp中空模板与空模块的用法实例
Nov 26 PHP
微信公众平台开发关注及取消关注事件的方法
Dec 23 PHP
PHP Try-catch 语句使用技巧
Feb 28 PHP
Joomla调用系统自带编辑器的实现方法
May 05 PHP
php把字符串指定字符分割成数组的方法
Mar 12 PHP
PHP商品秒杀问题解决方案实例详解【mysql与redis】
Jul 22 PHP
php使用redis的有序集合zset实现延迟队列应用示例
Feb 20 PHP
关于PHP中interface的用处详解
Jul 26 PHP
PHP实现Snowflake生成分布式唯一ID的方法示例
Aug 30 #PHP
Yii实现微信公众号场景二维码的方法实例
Aug 30 #PHP
Swoole源码中如何查询Websocket的连接问题详解
Aug 30 #PHP
PHP常用header头定义代码示例汇总
Aug 29 #PHP
PHP isset()及empty()用法区别详解
Aug 29 #PHP
PHP实现简单日历类编写
Aug 28 #PHP
PHP实现文件上传与下载
Aug 28 #PHP
You might like
农民C键的运用技巧
2020/03/04 星际争霸
不用mod_rewrite直接用php实现伪静态化页面代码
2008/10/04 PHP
PHP页面实现定时跳转的方法
2014/10/31 PHP
基于json的jquery地区联动效果代码
2011/07/06 Javascript
非常有用的40款jQuery 插件推荐(系列二)
2011/12/25 Javascript
javascript学习笔记(十八) 获得页面中的元素代码
2012/06/20 Javascript
javascript 事件处理程序介绍
2012/06/27 Javascript
使用jquery实现鼠标滑过弹出更多相关信息层附源码下载
2015/11/23 Javascript
Bootstrap学习笔记之css样式设计(1)
2016/06/07 Javascript
AngularJS基础 ng-hide 指令用法及示例代码
2016/08/01 Javascript
jquery弹出框插件jquery.ui.dialog用法分析
2016/08/20 Javascript
XMLHttpRequest Level 2 使用指南
2016/08/26 Javascript
js表单登陆验证示例
2016/10/19 Javascript
详解JavaScript中数组的reduce方法
2016/12/02 Javascript
详解Vuex中mapState的具体用法
2017/09/28 Javascript
VUE饿了么树形控件添加增删改功能的示例代码
2017/10/17 Javascript
JS 数组随机洗牌的实例代码
2018/09/12 Javascript
微信小程序实现分享朋友圈的图片功能示例
2019/01/18 Javascript
Vue 利用指令实现禁止反复发送请求的两种方法
2019/09/15 Javascript
vue实现在线学生录入系统
2020/05/30 Javascript
python生成器的使用方法
2013/11/21 Python
python开发之函数定义实例分析
2015/11/12 Python
在Linux系统上部署Apache+Python+Django+MySQL环境
2015/12/24 Python
Tornado Web Server框架编写简易Python服务器
2018/07/28 Python
详解配置Django的Celery异步之路踩坑
2018/11/25 Python
Django组件content-type使用方法详解
2019/07/19 Python
Python之字典对象的几种创建方法
2020/09/30 Python
图解CSS3制作圆环形进度条的实例教程
2016/05/26 HTML / CSS
大学毕业通用个人的求职信
2013/12/08 职场文书
微笑面对生活演讲稿
2014/05/13 职场文书
社区班子对照检查材料
2014/08/27 职场文书
服务行业演讲稿
2014/09/02 职场文书
增值税发票丢失证明
2015/06/19 职场文书
导游词之重庆渣滓洞
2020/01/08 职场文书
python全面解析接口返回数据
2022/02/12 Python
Python实战实现爬取天气数据并完成可视化分析详解
2022/06/16 Python