不推荐用PhpSpreadsheet导出百万行Excel,因其纯PHP实现需全量加载内存,易触发OOM和超时;应采用xlswriter C扩展流式写入+分块查询+异步任务+ZIP分片打包方案。

Webman 集成高性能 Excel 导出,**不推荐直接用 PhpSpreadsheet 一次性生成百万行文件**——它会吃光内存、触发超时、阻塞进程。真正可行的路径是:**用 xlswriter C 扩展流式写入 + 分块查询 + 异步触发 + ZIP 合并**。下面直说关键怎么做。
为什么不能用 PhpSpreadsheet 处理大数据导出
它纯 PHP 实现,所有数据先加载进内存再序列化为 XML,哪怕关掉缓存(setCachedObjectStorage)、用 fromArray,10 万行以上就大概率触发 Fatal error: Allowed memory size exhausted。Webman 是常驻内存的 Swoole 应用,一次卡死会影响整个 worker 进程,比传统 PHP-FPM 更危险。
- 单次导出 5 万行,
PhpSpreadsheet常驻内存占用轻松破 200MB - 导出过程中无法
ob_flush()或流式输出,浏览器只能干等 - 不支持真正的“边查边写”,必须等全部数据 fetch 完才能开始写文件
用 xlswriter 实现流式写入与即时下载
xlswriter 是 C 扩展,不依赖 DOM 或 XML 解析,直接二进制写入 .xlsx 文件,内存恒定在几 MB 级别,且支持 writeRow() 逐行写入。Webman 中可这样用:
- 安装:
pecl install xlswriter,并在php.ini加extension=xlswriter.so - 导出逻辑不走
response()->download(),而是打开php://output流,手动设 header - 必须调用
set_time_limit(0),否则 Swoole 默认 10 秒超时会中断 - 每写入 1 万行后调用
ob_flush()+flush(),避免 Nginx 缓冲导致前端无响应
示例关键片段:
$config = ['path' => sys_get_temp_dir() . '/'];
$excel = new \Vtiful\Kernel\Excel($config);
$file = $excel->fileName('export_' . date('YmdHis') . '.xlsx');
// 写表头
$file->header(['ID', '用户名', '创建时间']);
// 分页查库,每次取 5000 行
$offset = 0;
$limit = 5000;
do {
$rows = DB::table('users')->select('id', 'name', 'created_at')
->offset($offset)->limit($limit)->get();
foreach ($rows as $row) {
$file->writeRow([
(int)$row->id,
(string)$row->name,
$row->created_at->format('Y-m-d H:i:s')
]);
}
$offset += $limit;
ob_flush();
flush();
} while ($rows->count() === $limit);
// 输出到浏览器
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment; filename="export.xlsx"');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Content-Length: ' . filesize($file->output()));
readfile($file->output());
@unlink($file->output()); // 清理临时文件
超过 50 万行?必须分文件 + ZIP 打包
即使 xlswriter 内存友好,单个 .xlsx 文件超过 100 万行仍可能被 Excel 拒绝打开,或触发客户端解析失败。稳妥做法是:按 5 万行/文件切分,生成 export_1.xlsx、export_2.xlsx… 再用 ZipArchive 合并。
立即学习“PHP免费学习笔记(深入)”;
- 每个分片独立查库、独立写文件,互不影响;失败只影响该分片,可重试
- 临时文件统一放
/tmp/webman_export/,加时间戳和随机后缀防冲突 - ZIP 打包前检查所有分片是否完整,缺失则返回 500 并记录日志
- 打包完成后立即
unlink所有分片,只保留 ZIP,避免磁盘占满
注意:ZipArchive::addFile() 不支持流式添加,必须先落地文件再加入 ZIP。
Webman 下必须异步化,否则阻塞事件循环
Webman 基于 Swoole,所有导出操作若在 HTTP 请求中同步执行,会锁住当前 worker,导致其他请求排队。正确姿势是:前端点击后,后端立刻返回任务 ID,由 task 进程或 Redis 队列异步处理。
- 用 Webman 的
support\Task组件投递导出任务,传参包括用户 ID、查询条件、分片策略 - 导出完成将 ZIP 路径存入 Redis,设置 24 小时过期,同时发通知给前端轮询
- 前端轮询
/api/export/status?task_id=xxx,返回{ "status": "done", "url": "/download/xxx.zip" } - 下载接口仅做权限校验 +
readfile,不参与生成逻辑
这个环节最容易被忽略:很多人把异步理解成“另起一个 PHP-FPM 进程”,但在 Webman 里,必须用 Swoole 原生 task 或第三方队列(如 Redis + webman/redis-queue),否则还是同步阻塞。
while 查库写文件,就还没入门。



















