公司目前有一個需求,需要對一個日增量在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;//每次只從數據庫取100000條以防變量緩存太大 # 每隔$limit行,刷新一下輸出buffer,不要太大,也不要太小 $limit = 20000; $maxFileCount = 1000000; # 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); } }
最后將生成好的文件存入文件系統,上傳成功之后反轉導出狀態,前臺檢測到導出成功自動進行下載即可。