自己挖的坑自己填吧,今天咱就简单地利用swoole(实际上用我撸的那个沙雕一样的ti-rpc,上手会快一些)去实现这种【大量耗时数据导出】需求。但是,我还是偷了两点儿懒:
- 我实在懒得实现【数据库查询并生成csv或excel】这个功能了,这个地方我用一个sleep函数去模拟了一下
- 没有写网页而是用curl模拟了网页,模拟了点击【导出】和等待ajax轮训结果的用户行为
作为PM,我来说下大概的需求是怎样的:我们前段时间做的那个【搞附近】项目成功了,骗到了融资小目标:一个亿。现在是我们的运营需要一个网页能导出所有用户资料为excel文件的功能。因为用户量十分巨大,所以导出工作不可以使用PHP-FPM来实现,所以柱子在衡量了一下后决定采用swoole这种具备常驻内存特性的玩意来实现数据导出工作(老李去旅长那里背黑锅去了)。
在跟老赵报告了一下技术可行性后,柱子做的PPT里展示的具体技术流程是这样shai儿的:
- 当运营在网页上点击了【导出】按钮后,会向服务器发送一个ajax请求,请求中会带上参数:比如文件id。然后自此之后开始ajax轮训(依然需要带上文件id参数)文件状态,一秒钟一次
- 服务器收到该指令后,立马向网页客户端返回消息(此处一定要注意,要立马向网页返回消息,比如【开始处理】,此处利用了swoole异步特性)告诉运营已经开始处理了
- 然后紧接着第2步,服务器会向redis中写入一个文件处理状态标记,表示这个id的文件正在【处理中】
- 从数据库中读取数据,然后生成文件。具体演示里,此处柱子偷懒直接用sleep代替了整个处理过程。知道这叫什么吗?这就叫业务模型抽象能力...
- 文件处理完毕后,修改redis中文件处理状态标记为【处理完毕】,并开始将文件的下载链接拼接好(这个看你们把文件存哪儿了),把【文件下载链接】和【文件处理状态标记】一并返回给网页客户端
- 因为网页客户端还在保持一秒钟一次的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秒钟会看到如下结果,就相当于网页上【处理中】按钮变成【已完成,请点击下载】按钮:
自此,我们算实现了下面这个技术流程图: