公司目前有一個需求,需要對一個日增量在20萬+數據量的數據表中的數據進行可自定義條件篩選的導出數據,該功能需要對多個部門進行開發(fā)使用,要保證功能可用的前提下,盡量優(yōu)化體驗。
首先介紹一下當前可利用的資源:
1、MySQL - 一主庫雙從庫。
2、分布式服務器集群,選擇其中一臺中型機作為腳本執(zhí)行載體。
3、文件系統 - 可以支持上傳大數據量文件。
4、編程語言php。
技術難點:
1、數據太大,對服務器配置要求較高,導出過程中涉及數據的處理(例如各種ID轉換名稱等操作,我們這次需求這種太多了~~非常的坑)對內存消耗很大,其次涉及到文件壓縮,因此對CPU要求較高。
2、因為是跨系統部署,如果走接口,數據量隨隨便便上百M,傳輸速度太慢(項目是對外網開放的,然后數據只允許內網訪問),那么該如何解決?
3、數據安全性較高,需要對所有導出進行記錄,那么如何保證數據安全?
| 技術方案
第一步:設計數據庫,對所有導出任務進行實時記錄,也可以采用redis,為了方便數據的持久化,我最終采用了mysql數據庫的方案。表結構具體包括:ID、用戶ID、用戶名、發(fā)起請求時間、導出具體的參數(包括各個維度的參數選擇等,具體根據自身業(yè)務而定),任務是否正在處理標識(防止任務多次被處理),導出是否成功標識(可以與前一個用一個字段區(qū)分),刪除標識等(假刪除,便于記錄用戶實際操作日志)。
第二步:前臺界面編寫,具體包括參數選擇、導出記錄列表等,作用:觸發(fā)導出任務創(chuàng)建,記錄于導出表中,狀態(tài):待處理。
第三步:編寫導出腳本對任務進行監(jiān)控并處理,如果有導出任務自動對其執(zhí)行導出操作。
這里有一個小問題:為什么不在前臺觸發(fā)任務的時候直接執(zhí)行導出,而是有單獨的腳本來執(zhí)行導出呢?這就是現實業(yè)務導致的,因為我們對外開放的機器中有一些是配置很低的,為了保證導出的成功率,我們需要一臺配置較高的機器來獨立執(zhí)行導出任務。
| 導出流程
具體流程參考下圖
| 代碼實現
這里主要著重介紹一下導出腳本的代碼,其他步驟的代碼根據自己的業(yè)務自行編寫就可以了。
注意:因為數據量過大~一次性導出可想而知是不合理的,所以我使用了分頁導出的形式~
首先查詢數據總條數、然后通過每頁導出的條數來計算具體導出的頁數~
# 獲取數據總條數 $dataCount = Data_ExportModel::getExportZipTotalCount($params); $dataCount = $dataCount[0]['count_num']; # csv # 輸出Excel文件頭,可把user.csv換成你要的文件名 $mark = '/tmp/export'; $stepLen = 20000;//每次只從數據庫取100000條以防變量緩存太大 # 每隔$limit行,刷新一下輸出buffer,不要太大,也不要太小 $limit = 20000; $maxFileCount = 1000000; # buffer計數器 $cnt = 0; $head = self::initColumnDataV2(); // 表頭部分根據自身業(yè)務自行調整 $fileNameArr = array(); $salesStatisticsData = array(); $startLimitId = 0;
首次導出的每頁條數我定的10萬條,后來發(fā)現對內存消耗過大,改成了兩萬條,這樣的導出速度會慢一點,建議五萬條比較適中一點。
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; // 跳出循環(huán)
}
# 查詢數據
$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;
}
# 數據處理部分,根據自身業(yè)務自行定義,注意中文轉碼
$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)) {
// 首次執(zhí)行檢查生成的壓縮文件是否存在失敗,進行二次嘗試。。。
$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; // 防止出現異常情況導致死循環(huán),最多重試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);
}
}
最后將生成好的文件存入文件系統,上傳成功之后反轉導出狀態(tài),前臺檢測到導出成功自動進行下載即可。






