文章来自于:博客

分享教程

链接放在公众号里是最好的,过期了就可以修改,所以这就是不放在文章里的原因,有需要的可以关注“PHP自学中心”,回复对应的关键词,获取链接与提取码。感谢大家的支持与信任

使用正则动态修改配置文件链接: SESSION示例之购物车链接:https://pan.baidu.com/s/1_d6-UqKxmGCR2azhi836Qw 密码:本公众号回复  即可领取

文章正文

公司目前有一个需求,需要对一个日增量在20万+数据量的数据表中的数据进行可自定义条件筛选的导出数据,该功能需要对多个部门进行开发使用,要保证功能可用的前提下,尽量优化体验。

首先介绍一下当前可利用的资源:

1、MySql - 一主库双从库。

2、分布式服务器集群,选择其中一台中型机作为脚本执行载体。

3、文件系统 - 可以支持上传大数据量文件。

4、编程语言PHP。

技术难点:

1、数据太大,对服务器配置要求较高,导出过程中涉及数据的处理(例如各种ID转换名称等操作,我们这次需求这种太多了~~非常的坑)对内存消耗很大,其次涉及到文件压缩,因此对CPU要求较高。

2、因为是跨系统部署,如果走接口,数据量随随便便上百M,传输速度太慢(项目是对外网开放的,然后数据只允许内网访问),那么该如何解决?

3、数据安全性较高,需要对所有导出进行记录,那么如何保证数据安全?

| 技术方案

第一步:设计数据库,对所有导出任务进行实时记录,也可以采用redis,为了方便数据的持久化,我最终采用了mysql数据库的方案。表结构具体包括:ID、用户ID、用户名、发起请求时间、导出具体的参数(包括各个维度的参数选择等,具体根据自身业务而定),任务是否正在处理标识(防止任务多次被处理),导出是否成功标识(可以与前一个用一个字段区分),删除标识等(假删除,便于记录用户实际操作日志)。

第二步:前台界面编写,具体包括参数选择、导出记录列表等,作用:触发导出任务创建,记录于导出表中,状态:待处理。

第三步:编写导出脚本对任务进行监控并处理,如果有导出任务自动对其执行导出操作。

这里有一个小问题:为什么不在前台触发任务的时候直接执行导出,而是有单独的脚本来执行导出呢?这就是现实业务导致的,因为我们对外开放的机器中有一些是配置很低的,为了保证导出的成功率,我们需要一台配置较高的机器来独立执行导出任务。

| 导出流程

具体流程参考下图

| 代码实现

这里主要着重介绍一下导出脚本的代码,其他步骤的代码根据自己的业务自行编写就可以了。

注意:因为数据量过大~一次性导出可想而知是不合理的,所以我使用了分页导出的形式~

首先查询数据总条数、然后通过每页导出的条数来计算具体导出的页数~

# 获取数据总条数$dataCount = Data_ExportModel::getExportZipTotalCount($params);$dataCount = $dataCount[0]['count_num'];# csv# 输出Excel文件头,可把user.csv换成你要的文件名$mark = '/tmp/export';$stepLen = 20000;//每次只从数据库取条以防变量缓存太大# 每隔$limit行,刷新一下输出buffer,不要太大,也不要太小$limit = 20000;$maxFileCount = 0;# buffer计数器$cnt = 0;$head = self::initColumnDataV2(); // 表头部分根据自身业务自行调整$fileNameArr = array();$salesStatisticsData = array();$startLimitId = 0;

首次导出的每页条数我定的10万条,后来发现对内存消耗过大,改成了两万条,这样的导出速度会慢一点,建议五万条比较适中一点。

for ($j = 0; $j < ceil($dataCount / $maxFileCount); $j++) {    $startSelect = ceil($maxFileCount / $stepLen)*$j;    $fileCsvName = $mark . '_'.$j*$maxFileCount.'_' . ($j+1)*$maxFileCount . '.csv';    $fp = fopen($fileCsvName, 'w'); //生成临时文件    $fileNameArr[] = $fileCsvName;    # 将数据通过fputcsv写到文件句柄    fputcsv($fp, $head);    for ($i = 0; $i < 50; $i++) { // 单个文件支持100万数据条数        $startNum = $j*$maxFileCount + $i*$limit;        if ($startNum > $dataCount) {            break;  // 跳出循环        }        # 查询数据        $dataSource = Data_ExportModel::getExportZipTotalInfo($params, $startNum, $stepLen, $startLimitId);        $endMicroTime = microtime(true);        printf("\n[%s -> %s] Begin Time : %s, End Time : %s, Total Count : %s, CostTime: %s.\n", __CLASS__, __FUNCTION__, $params['begin_date'], $params['end_date'], count($dataSource), ($endMicroTime - $startMicroTime));        if (empty($dataSource)) {            continue;        }        $endMicroTime = microtime(true);        foreach ($dataSource as $_key => $_data) {            $cnt++;            if ($limit == $cnt) {                # 刷新一下输出buffer,防止由于数据过多造成问题                ob_flush();                flush();                $cnt = 0;            }            # 数据处理部分,根据自身业务自行定义,注意中文转码            $salesStatisticsData['name'] = iconv('utf-8', 'GB18030', $salesStatisticsData['c_name']);            fputcsv($fp, $salesStatisticsData);        }    }    fclose($fp);  # 每生成一个文件关闭}# 进行多文件压缩$zip = new ZipArchive();$number = rand(1000,9999);$filename = $mark."_".$params['begin_date']."_".$params['end_date'] ."_".$number. ".zip";$zip->open($filename, ZipArchive::CREATE);   //打开压缩包foreach ($fileNameArr as $file) {    $zip->addFile($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);    }    $zip->open($filename, ZipArchive::CREATE);   //打开压缩包    foreach ($fileNameArr as $file) {        $zip->addFile($file, basename($file));   //向压缩包中添加文件    }    $zip->close();  //关闭压缩包}if (file_exists($filename)) {    $content = file_get_contents($filename);    // 解决读取文件偶尔出现失败的问题,第一读出为空则尝试第二次读取    $forNum = 0;    while (!$content) {        $forNum++;        @$content = file_get_contents($filename);        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);    }}

最后将生成好的文件存入文件系统,上传成功之后反转导出状态,前台检测到导出成功自动进行下载即可。