应使用cursor()或chunk()替代select()避免OOM,配合PhpSpreadsheet垃圾回收和禁用缓存,优先导出CSV,并通过异步队列防并发崩溃。

ThinkPHP 6.x 分页导出大表数据时内存爆掉
直接用 Db::table()->select() 拉全量数据再写 Excel,10 万行起步就 OOM。根本原因不是 Excel 库慢,是 ThinkPHP 默认把全部结果集加载进 PHP 内存,且 Collection 对象自带引用和序列化开销。
- 用
cursor()替代select():它返回可遍历的PDOStatement,逐行读取,不缓存整张结果集 - 配合
chunk()更稳妥:按固定条数分块(如chunk(500)),每块处理完自动释放内存,适合需中间计算的场景 - 避免在循环里反复调用
Db::table()->where(...)->find()—— 单条查 10 万次 = 10 万次连接/解析开销,比chunk()慢一个数量级
使用 PhpSpreadsheet 导出时频繁触发 GC 导致卡顿
每次循环调用 $sheet->setCellValue() 都会新建 Cell 对象,PhpSpreadsheet 默认开启内存缓存,不手动清理就会越积越多。尤其在 chunk() 循环里没重置工作表状态,第二块开始写入速度断崖下跌。
- 每 chunk 处理完后调用
$spreadsheet->garbageCollect(),强制释放未引用的 Cell 和 Style 对象 - 禁用单元格缓存:初始化时传参
['cache' => 'none'],或设Settings::setCache('none') - 不要在循环里反复
clone $sheet或createSheet(),单文件导出只用一个Worksheet实例
导出 CSV 替代 XLSX 能省掉 80% 内存但牺牲格式
如果用户只要数据、不要合并单元格/颜色/公式,CSV 是更务实的选择 —— 它本质是纯文本流式写入,和数据库游标天然契合,内存占用基本恒定。
- 用
fopen('php://output', 'w')直接输出,配合fputcsv(),无需加载任何第三方库 - 注意字段含逗号、换行符、双引号时必须用
fputcsv()自动转义,手拼字符串大概率导出乱码 - ThinkPHP 的
Response可直接 write 原始 CSV 流,设置 header:Content-Type: text/csv; charset=utf-8和Content-Disposition: attachment; filename="data.csv"
导出接口被并发刷崩?得加防抖 + 异步队列
即使做了内存优化,同步导出仍会阻塞 PHP-FPM 进程。10 个用户同时点“导出”,就是 10 个长耗时进程占着 worker,新请求排队超时。
立即学习“PHP免费学习笔记(深入)”;
- 前端按钮点击后立即禁用,并显示“已提交,后台生成中”,后端立刻返回任务 ID(如
task_20240520143022_abc123) - 真正查询+写文件逻辑扔进队列(如 Redis + think-queue),避免阻塞 HTTP 请求周期
- 用独立的临时目录存生成的 Excel 文件,过期自动清理;前端轮询
/export/status?task_id=xxx查进度,返回下载地址
最易被忽略的是临时文件权限和清理策略 —— 队列进程和 Web 进程用户不一致时,可能写入失败;没设 TTL 的临时文件堆满磁盘,比内存问题更难排查。


















