自己挖的坑自己填吧,今天咱就简单地利用swoole(实际上用我撸的那个沙雕一样的ti-rpc,上手会快一些)去实现这种【大量耗时数据导出】需求。但是,我还是偷了两点儿懒:
作为PM,我来说下大概的需求是怎样的:我们前段时间做的那个【搞附近】项目成功了,骗到了融资小目标:一个亿。现在是我们的运营需要一个网页能导出所有用户资料为excel文件的功能。因为用户量十分巨大,所以导出工作不可以使用PHP-FPM来实现,所以柱子在衡量了一下后决定采用swoole这种具备常驻内存特性的玩意来实现数据导出工作(老李去旅长那里背黑锅去了)。
在跟老赵报告了一下技术可行性后,柱子做的PPT里展示的具体技术流程是这样shai儿的:
完美
在正式开始贴上可供大家复制粘贴的代码前,请你准备好下列物料:
CV BOY们,项目代码github地址如下:
https://github.com/elarity/wechat-official-accounts-demo-code
运行方式:
但是!请看这边!代码还是要讲解、分析的
服务端业务代码分析
<?php
namespace Application\Controller;
use Application\Model 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 = \System\Component\Di::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模拟的网页端代码
<?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秒钟会看到如下结果,就相当于网页上【处理中】按钮变成【已完成,请点击下载】按钮:
自此,我们算实现了下面这个技术流程图: