如何在Yii2框架结合MongoDB环境下高效导出2000万条数据至PDF文件?
针对你在Yii2+MongoDB环境下导出2000万条数据到PDF的性能问题,我整理了几个核心优化方向,结合你的代码场景给出具体建议:
一、数据查询层优化(MongoDB + Yii2)
这部分是性能瓶颈的核心源头,慢查询会直接拖垮整个导出流程:
- 避免一次性加载全量数据:你当前设置
pagination => false会把2000万条数据全部加载到内存,不仅速度慢还极易引发内存溢出。建议改成分批查询,每次拉取固定数量的数据(比如1万条)处理:// 替换原ActiveDataProvider的全量查询逻辑,改用分页循环 $totalCount = $query->count(); $pageSize = 10000; // 可根据服务器内存调整 $pages = ceil($totalCount / $pageSize); for ($page = 1; $page <= $pages; $page++) { $batchData = $query->offset(($page-1)*$pageSize)->limit($pageSize)->all(); // 处理当前批次数据,生成对应HTML片段 } - 给MongoDB创建复合索引:你的查询涉及
isParent、tm_id、callstatus、hangup、start_epoch/end_epoch等过滤条件,创建匹配查询优先级的复合索引能大幅提升查询速度:
注意:范围查询字段(如// 在MongoDB Shell中执行索引创建命令 db.canned_reports.createIndex( { isParent: 1, tm_id: 1, callstatus: 1, hangup: 1, start_epoch: -1 } )start_epoch)要放在索引的最后位置。 - 减少ORM开销:如果ActiveRecord的封装带来额外性能损耗,可以直接使用Yii2的MongoDB QueryBuilder或原生驱动查询,跳过ActiveRecord实例化过程:
$mongoDb = Yii::$app->mongodb->getDatabase(); $collection = $mongoDb->getCollection('canned_reports'); // 构建原生查询条件 $filter = [ 'isParent' => 1, 'tm_id' => (string)Yii::$app->user->identity->tm_id, 'callstatus' => ['$ne' => 'completed'], 'hangup' => 'ORIGINATOR_CANCEL', 'start_epoch' => ['$gte' => $startEpoch, '$lte' => $endEpoch] ]; // 分批获取数据,减少内存占用 $cursor = $collection->find($filter)->sort(['start_epoch' => -1])->batchSize(10000); foreach ($cursor as $document) { // 直接处理原生文档数据 }
二、PDF生成层优化(wkhtmltopdf)
wkhtmltopdf对超大HTML内容的渲染效率极低,核心优化思路是分批生成小PDF,最后合并:
- 分批生成+合并PDF:不要一次性把所有HTML传给
addPage,每处理一批数据就生成一个独立的临时PDF文件,最后用工具合并成最终文件:$tempPdfs = []; $pageSize = 10000; $totalCount = $query->count(); $pages = ceil($totalCount / $pageSize); $tempDir = sys_get_temp_dir(); for ($page = 1; $page <= $pages; $page++) { $batchData = $query->offset(($page-1)*$pageSize)->limit($pageSize)->all(); // 渲染当前批次的表格HTML(用renderPartial避免加载布局) $batchHtml = $this->renderPartial('_report_table', ['data' => $batchData]); $tempPdf = new Pdf([ 'binary' => '/usr/local/bin/wkhtmltopdf', 'options' => ['disable-smart-shrinking', 'no-outline', 'disable-javascript'], ]); $tempPdf->addPage($batchHtml); $tempPath = $tempDir . '/batch_' . $page . '.pdf'; $tempPdf->saveAs($tempPath); $tempPdfs[] = $tempPath; } // 使用pdftk或Ghostscript合并临时PDF(需提前安装对应工具) $finalPdfPath = '/path/to/your/final_report.pdf'; exec("pdftk " . implode(' ', $tempPdfs) . " cat output " . $finalPdfPath); // 清理临时文件 foreach ($tempPdfs as $path) { unlink($path); } - 优化HTML渲染效率:
- 简化表格样式:去掉不必要的CSS、JavaScript,wkhtmltopdf对复杂CSS的渲染耗时极高;
- 扁平化DOM结构:避免嵌套过深的HTML标签,减少渲染计算量;
- 提前缓存总数:不要在PDF生成时重复调用
$dataProvider->query->count(),提前查询一次并缓存结果。
- wkhtmltopdf参数调优:添加以下参数减少渲染负担:
$options = [ 'dpi' => 96, 'image-quality' => 80, // 降低图片质量(如果报表无图片可设为0或禁用) 'disable-smart-shrinking', 'no-outline', 'disable-javascript', // 禁用JS(无交互需求时建议开启) 'no-images', // 报表无图片时直接禁用图片加载 'lowquality', // 可选:降低输出质量换取速度 ];
三、架构层面优化
2000万条数据的导出属于长时任务,同步请求必然会超时,必须改成异步处理:
- 用Yii2队列异步执行导出:把导出任务放到队列(如Redis Queue、RabbitMQ),由后台Worker进程处理,完成后通知用户下载:
- 创建导出任务类:
class ExportPdfJob extends BaseObject implements \yii\queue\JobInterface { public $params; // 接收查询参数 public function execute($queue) { // 这里放入查询+PDF生成的完整逻辑 // 生成完成后可通过邮件、站内信通知用户,或把文件路径存入数据库供下载 } } - 控制器中推送任务到队列:
Yii::$app->queue->push(new ExportPdfJob(['params' => $params])); return $this->asJson(['message' => '导出任务已启动,完成后将通知你']);
- 创建导出任务类:
- 分块导出(可选):如果用户不需要单一超大PDF,可以按时间范围或其他维度拆分多个小文件,用户可分别下载,同时降低单文件处理压力。
四、其他小技巧
- 内存监控与释放:在批次循环中定期用
unset()释放不再需要的变量,避免内存泄漏; - MongoDB连接优化:确保使用合适的
batchSize参数,减少数据库与应用的网络往返次数; - 临时扩容服务器:wkhtmltopdf是CPU密集型任务,临时提升服务器CPU核心数、内存资源,能显著提升渲染速度。
内容的提问来源于stack exchange,提问作者p1992
相关产品推荐
相关产品推荐

