震惊!北京一男子竟然用swoole做了这种事!

2019-11-13 16:23:41 浏览数 (1)

自己挖的坑自己填吧,今天咱就简单地利用swoole(实际上用我撸的那个沙雕一样的ti-rpc,上手会快一些)去实现这种【大量耗时数据导出】需求。但是,我还是偷了两点儿懒:

  • 我实在懒得实现【数据库查询并生成csv或excel】这个功能了,这个地方我用一个sleep函数去模拟了一下
  • 没有写网页而是用curl模拟了网页,模拟了点击【导出】和等待ajax轮训结果的用户行为

作为PM,我来说下大概的需求是怎样的:我们前段时间做的那个【搞附近】项目成功了,骗到了融资小目标:一个亿。现在是我们的运营需要一个网页能导出所有用户资料为excel文件的功能。因为用户量十分巨大,所以导出工作不可以使用PHP-FPM来实现,所以柱子在衡量了一下后决定采用swoole这种具备常驻内存特性的玩意来实现数据导出工作(老李去旅长那里背黑锅去了)。

在跟老赵报告了一下技术可行性后,柱子做的PPT里展示的具体技术流程是这样shai儿的:

  1. 当运营在网页上点击了【导出】按钮后,会向服务器发送一个ajax请求,请求中会带上参数:比如文件id。然后自此之后开始ajax轮训(依然需要带上文件id参数)文件状态,一秒钟一次
  2. 服务器收到该指令后,立马向网页客户端返回消息(此处一定要注意,要立马向网页返回消息,比如【开始处理】,此处利用了swoole异步特性)告诉运营已经开始处理了
  3. 然后紧接着第2步,服务器会向redis中写入一个文件处理状态标记,表示这个id的文件正在【处理中】
  4. 从数据库中读取数据,然后生成文件。具体演示里,此处柱子偷懒直接用sleep代替了整个处理过程。知道这叫什么吗?这就叫业务模型抽象能力...
  5. 文件处理完毕后,修改redis中文件处理状态标记为【处理完毕】,并开始将文件的下载链接拼接好(这个看你们把文件存哪儿了),把【文件下载链接】和【文件处理状态标记】一并返回给网页客户端
  6. 因为网页客户端还在保持一秒钟一次的ajax轮训,所以当它发现服务器返回了【处理完毕】状态,所以它就取【文件下载链接】的值并同时告诉运营:您要的文件已经O jb K,点击下载吧

完美

在正式开始贴上可供大家复制粘贴的代码前,请你准备好下列物料:

  • linux环境,ubuntu、centos都行...
  • swoole 1.9.22(http://pecl.php.net/get/swoole-1.9.22.tgz)
  • MySQL(虽然用sleep抽象了,但是看看swoole里怎么用mysql吧)
  • Redis(其实有洁癖的人可以用swoole-table来代替redis)
  • Ti-RPC(https://github.com/elarity/ti-rpc)

CV BOY们,项目代码github地址如下:

https://github.com/elarity/wechat-official-accounts-demo-code

运行方式:

  • 服务端:git clone下项目后,进入到ti-rpc根目录,然后php index.php start(PS:记得配置你的MySQL数据库账号密码,在System->Library->Mysql.php的第59行,不然MySQL可能会连接不上)
  • 网页客户端:进入到ti-rpc根目录中,再进入到example目录中,执行php http_client.php

但是!请看这边!代码还是要讲解、分析的


服务端业务代码分析

代码语言:javascript复制
<?php
namespace ApplicationController;
use ApplicationModel as Model;
class Account{
/*  
   * @desc : param['file_id'] : 文件的唯一id
             在实际业务里,你可以用[文件id uid]保证唯一
   */
public function mysql2excel( $param ){
    // file_id参数验证,必须d得有这个参数
    if ( empty( $param['param']['file_id'] ) ) { 
      return []; 
    }   
    $s_file_id = $param['param']['file_id'];
    // 获取服务容器并从中取出redis操作句柄
    $o_di    = SystemComponentDi::getInstance();
    $o_redis = $o_di->get('redis');
    // 根据客户端/网页中传来的file_id参数设置内存标记
    // 我就偷懒了昂,直接用redis来记录文件状态 
    $s_file_export_state = $o_redis->get( $s_file_id );
    // 如果存在这个标记,表示文件正在【处理中】或者【已完成】
    if ( false !== $s_file_export_state ) { 
      // 默认给一个空下载链接,如果已经处理完毕,你按照你的具体文件存放路径规律可以直接将下载地址拼接出来
      $s_download_link = 'done' == $s_file_export_state ? 'http://www.baidu.com/1.zip' : '' ;
      return array(
      'state' => $s_file_export_state,
      'data'  => $s_download_link,
      );
    }
    // 如果不存在这个标记,就直接进入到导出处理逻辑中 
    else {

      $s_file_export_state = 'processing';
       // 向redis中写入文件【处理中】标记
      $b_set_ret = $o_redis->set( $s_file_id, $s_file_export_state );
      if ( !$b_set_ret ) {
        return array(
          'code'    => -1,
          'message' => '写入redis文件标记失败',
        );
      }

      // 从服务容器中获取mysql资源句柄
      // 模拟30秒钟文件处理过程
      // 你可以在下面这里处理你的数据查询逻辑,以及查询完毕后如果生成为csv或者excel文件的逻辑
      // 这个数据库查询没啥用,就是顶多演示一下swoole里怎么搞MySQL数据查询
      $o_mysql    = $o_di->get('mysql');
      $a_user_ret = $o_mysql->query( "select * from bilibili_user_info limit 10" );
      sleep( 30 );

       // 处理完毕后,改写redis中文件标记为【已完成】
      $s_file_export_state = 'done';
      $o_redis->set( $s_file_id, $s_file_export_state );
      return array(
        'code' => 0,
      );

    }
    return [];
  }

}

当客户端执行开始执行代码后,我们注意下服务端的demo代码会打印如下log:

你们这是什么意思吗?注意看第一个进程PID为5561的进程自从第一次出现后,就再也没有出现过,其他PID则是轮流重复出现,为什么?因为5561就是正在处理【数据导出为文件】任务的进程,作为业务为同步阻塞模型的代码,此时该进程不会相应其他任何请求的。所以我们这个demo的一个缺陷就是:如果所有进程都在处理【数据导出为文件】任务了,那么就会出现网页客户端ajax轮训无法查询到状态的情况。


CURL模拟的网页端代码

代码语言:javascript复制
<?php
/*
@desc : 利用curl封装的简单对http的客户端演示案例
 */
// 封装curl方法
function curl_init_param( $curl, $json_data ) {
  curl_setopt_array( $curl, array(
    CURLOPT_PORT      => 6666,
    CURLOPT_URL       => "http://127.0.0.1:6666/",
    CURLOPT_ENCODING  => "",
    CURLOPT_MAXREDIRS => 10,
    CURLOPT_TIMEOUT   => 30,
    CURLOPT_HTTP_VERSION   => CURL_HTTP_VERSION_1_1,
    CURLOPT_CUSTOMREQUEST  => "POST",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POSTFIELDS     => $json_data,
    CURLOPT_HTTPHEADER     => array(
      "cache-control: no-cache",
      "postman-token: efe52804-aa89-8c4d-01ae-5e9a27012312"
    ),
  ) );
}
// 文件名称
$s_file_id = '123456789123456';
// 模拟网页点击【导出】按钮后,发出【导出】命令请求...
// 构造请求参数.
$sn = array(
  // ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
  // type=SN的意思就是:请求触发后,服务器立马返回消息,客户端不会被阻塞一直等待结果
  // 在本次业务就是满足运营点击完【导出】按钮后的情境
  // ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
  'type' => 'SN',
  'requestId' => time(),
   // 这里就是构建的具体参数,我们会调用Account的mysql2excel方法,参数是file_id
  'param' => array(
  'model' => 'Account',
  'method' => 'mysql2excel',
  'param' => array(
    'file_id'  => $s_file_id,
   ),
  ),
);
$json_data = json_encode( $sn );
$curl      = curl_init();
curl_init_param( $curl, $json_data );
$response  = curl_exec( $curl );
$err       = curl_error( $curl );
if ( $err ) {
  echo "cURL Error #:" . $err.PHP_EOL;
  exit();
}
print_r( json_decode( $response, true ) );

// 点击完毕【导出】按钮后,开始模拟ajax轮训状态,一秒钟一次...
while ( 1 ) {
  sleep( 1 );
  // 构造请求参数.
  $sw = array(
    // ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
    // type=SW的意思就是:请求触发后,服务器不会马上返回请求,而是一直到处理完毕数据后才返回给客户端
    // 此处就是ajax轮训文件处理状态,这个是要等服务器从redis里取出状态后,才能返回给网页客户端的,所以
    // 必须阻塞等待。其实这里就是和传统的php-fpm是一回事。
    // ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
    'type' => 'SW',
    'requestId' => time(),
    'param' => array(
      'model' => 'Account',
      'method' => 'mysql2excel',
      'param' => array(
        'file_id'  => $s_file_id,
       ),
     ),
  );
  $json_data = json_encode( $sw );
  curl_init_param( $curl, $json_data );
  $response  = curl_exec( $curl );
  $err       = curl_error( $curl );
  if ( $err ) {
    echo "cURL Error #:" . $err.PHP_EOL;
  } else {
    print_r( json_decode( $response, true ) );
  }
}

php http_client.php执行了网页客户端后,我们等待30秒钟会看到如下结果,就相当于网页上【处理中】按钮变成【已完成,请点击下载】按钮:

自此,我们算实现了下面这个技术流程图:

0 人点赞