PHP大数据量导出优化方案:20万+日增量数据表的自定义筛选与导出技巧
链接放在公众号里是较为妥当的。因为放在公众号里,若过期了还可以进行修改。这便是不将其放在文章里的缘由所在。倘若有需要,大家可以关注“PHP 自学中心”,然后回复对应的关键词,即可获取链接与提取码。感谢大家一直以来的支持与信任。

使用正则动态修改配置文件
链接:https://pan.baidu.com/s/155RKUawCqdZsUmLOA46JNw
SESSION示例之购物车
链接:https://pan.baidu.com/s/1_d6-UqKxmGCR2azhi836Qw
密码:本公众号回复 21112018 即可领取
文章正文
公司当前有一个需求,即要对一个日增量为 20 万+的数据量的数据表中的数据进行可按自定义条件筛选后导出。此功能需对多个部门进行开发以供使用,在保证功能可用的同时,要尽量将体验进行优化。
首先介绍一下当前可利用的资源:
1、MySql - 一主库双从库。
2、分布式服务器集群,选择其中一台中型机作为脚本执行载体。
3、文件系统 - 可以支持上传大数据量文件。
4、编程语言PHP。
技术难点:
数据较大,这使得对服务器配置有较高要求。在导出过程中,会涉及数据的处理,像各种 ID 转换为名称等操作,而此次需求中这类操作很多,非常麻烦。这些数据处理对内存消耗很大。此外,还涉及文件压缩,所以对 CPU 要求也较高。
因为是跨系统进行部署,若采用走接口的方式,数据量往往会轻易达到上百 M,这样会导致传输速度非常慢。而该项目是对外网开放的,同时数据又只允许内网访问,那么在这种情况下,应该如何去解决呢?
数据安全性较高,并且需要对所有导出进行记录。那么,怎样才能保证数据的安全呢?
| 技术方案
设计数据库,对所有导出任务进行实时记录。可以采用 redis 来实现这一功能。为了方便数据的持久化,最终选择了 mysql 数据库的方案。删除标识等,此为假删除,便于记录用户的实际操作日志。
第二步是进行前台界面编写,其中包含参数选择以及导出记录列表等内容。其作用是触发导出任务的创建,并且会将相关记录记录于导出表中,目前的状态是待处理。
第三步,编写导出脚本,对任务进行监控并处理。若存在导出任务,便自动对其执行导出操作。
这里存在一个小问题:为何不在前台触发任务时直接进行导出,却要用单独的脚本来执行导出呢?这是由现实业务所导致的。因为我们对外开放的机器里,有一些机器的配置是很低的。为了确保导出的成功率,我们需要一台配置较高的机器来单独执行导出任务。
| 导出流程
具体流程参考下图
| 代码实现
这里主要介绍导出脚本的代码,其他步骤的代码需根据自身业务自行编写。
由于数据量非常大,一次性将所有数据导出这种做法是不合理的,因此我采用了分页导出的方式。
先查询数据的总条数,接着依据每页导出的条数去计算具体要导出的页数。
# 获取数据总条数
Data_ExportModel 获取了导出压缩包的总数,总数为 $dataCount,获取的依据是传入的参数 $params。
$dataCount = $dataCount[0]['count_num'];
# csv
输出 Excel 文件的头,能够将 user.csv 替换为你所需要的文件名。
$mark = '/tmp/export';
$stepLen = 20000;//每次只从数据库取100000条以防变量缓存太大
每隔$limit行就刷新一下输出buffer,这个buffer既不要太大,也不要太小。
$limit = 20000;
$maxFileCount = 1000000;
# buffer计数器
$cnt = 0;
$head = self::initColumnDataV2(); // 表头部分根据自身业务自行调整
$fileNameArr = array();
$salesStatisticsData = array();
$startLimitId = 0;
首次导出时,我设定每页的条数为 10 万条。之后发现这对内存消耗过大,于是将其改成了 2 万条。这样一来,导出速度会变慢一些。我建议每页的条数为 5 万条,这样比较适中。
for ($j = 0; $j < ceil($dataCount / $maxFileCount); $j++) {
$startSelect 等于 $maxFileCount 除以 $stepLen 的向上取整结果再乘以 $j 。
$fileCsvName = $mark . '_'.$j*$maxFileCount.'_' . ($j+1)*$maxFileCount . '.csv';
$fp 被用于打开 $fileCsvName 这个文件。'w'); //生成临时文件
$fileNameArr 包含了 $fileCsvName ;$fileNameArr 被赋值为 $fileCsvName ;$fileCsvName 被添加到 $fileNameArr 中 。
# 将数据通过fputcsv写到文件句柄
fputcsv($fp, $head);
for ($i = 0; $i < 50; $i++) { // 单个文件支持100万数据条数
$startNum 等于 $j 乘以 $maxFileCount 再加上 $i 乘以 $limit ;$startNum 是通过这种方式计算得出的 ;$startNum 的值由 $j 、$maxFileCount 、$i 和 $limit 共同决定 。
if ($startNum > $dataCount) {
break; // 跳出循环
}
# 查询数据
Data_ExportModel 用于获取导出压缩包的总信息,其获取的方式为调用 getExportZipTotalInfo 方法,并传入参数 $params、$startNum、$stepLen 和 $startLimitId,获取到的结果赋值给 $dataSource。
$endMicroTime 等于 microtime 函数的返回值。true);
printf([%s 转换为%s]开始时间:%s,结束时间:%s,总计数:%s,耗时:%s。, __CLASS__, __FUNCTION__, $params['begin_date'], $params['end_date'使用 count 函数获取 $dataSource 的数量,然后计算 ($endMicroTime - $startMicroTime) 的值。
if (empty($dataSource)) {
continue;
}
$endMicroTime 等于 microtime 函数的返回值。true);
foreach对于 ($dataSource 作为 $_key 并映射到 $_data) {
$cnt++;
if ($limit == $cnt) {
# 刷新一下输出buffer,防止由于数据过多造成问题
ob_flush();
flush();
$cnt = 0;
}
# 数据处理部分,根据自身业务自行定义,注意中文转码
销售统计数据中,各个时间段的销售数据各不相同。有的时间段销售数据较高,有的时间段销售数据较低。不同时间段的销售数据差异明显,这反映了市场需求的变化情况。销售数据的高低起伏与市场的动态紧密相关,通过对销售统计数据的分析,可以更好地了解市场的需求趋势,为企业的决策提供依据。'name'] = iconv('utf-8', 'GB18030', $salesStatisticsData['c_name']);
使用 fputcsv 函数将 $salesStatisticsData 写入到 $fp 中。
}
}
fclose($fp); # 每生成一个文件关闭
}
# 进行多文件压缩
$zip = new ZipArchive();
$number = rand(1000,9999);
$filename = $mark."_".$params['begin_date']."_".$params['end_date'] ."_".$number. ".zip";
打开名为 $filename 的文件,并创建一个 ZipArchive 对象,然后将其赋值给 $zip 变量,执行 $zip->open 操作。//打开压缩包
foreach ($fileNameArr as $file) {
$zip 把文件 $file 添加到其中,使用 basename($file) 作为添加后的文件名。//向压缩包中添加文件
}
$zip->close(); //关闭压缩包
if (!file_exists($filename)) {
首次执行检查,查看生成的压缩文件是否存在。若存在失败情况,则进行二次尝试。
$endMicroTime = microtime(true);
# 进行二次多文件压缩
$number = rand(1000,9999);
$filename = $mark."_".$params['begin_date']."_".$params['end_date'] ."_".$number. ".zip";
if (file_exists($filename)) {
unlink($filename);
}
使用 ZipArchive 类的 create 模式打开名为 $filename 的文件,并将其存储在 $zip 变量中。//打开压缩包
foreach ($fileNameArr as $file) {
$zip 向其中添加了一个文件,这个文件是 $file,添加后的文件名是 basename($file) 。//向压缩包中添加文件
}
$zip->close(); //关闭压缩包
}
if (file_exists($filename)) {
获取文件内容的操作是通过 file_get_contents 函数来实现的,将其赋值给变量 $content,其中的参数是 $filename 。
解决读取文件偶尔出现失败的情况,首先判断第一次读出是否为空,如果为空则尝试进行第二次读取。
$forNum = 0;
while (!$content) {
$forNum++;
使用 file_get_contents 函数获取了 $filename 对应的内容,并将其赋值给了 @$content 。
if ($forNum > 10) {
break; // 防止出现异常情况导致死循环,最多重试10次
}
}
} else {
$endMicroTime = microtime(true);
# 删除临时文件,防止占用空间
foreach ($fileNameArr as $file) {
if (is_file($file)) {
unlink($file);
}
}
// 记录错误日志并且报警
return false;
}
# 删除临时文件,防止占用空间
foreach ($fileNameArr as $file) {
if (is_file($file)) {
unlink($file);
}
}
最后把生成好的文件存到文件系统里,在上传成功之后把导出状态反转,前台一旦检测到导出成功,就会自动进行下载。
以上是文章的全部内容。若有学习与经验交流需求,可添加微信成为好友。一同学习,进行交流并取得进步!请备注:码农