应使用 StreamedResponse 流式导出万级数据,通过 chunkById 分批查询、fputcsv 写入 php://output、添加 BOM 头防中文乱码,并调大 Nginx/PHP-FPM 超时设置。

直接用 response()->stream() 避免内存爆炸
导出万级以上数据时,response()->download() 会先把整个 CSV 写进内存再吐给浏览器,极易触发 Allowed memory size exhausted。必须改用流式响应,边查边写、边写边发。
关键点是:不拼接完整字符串,不调用 collect()->map()->toArray() 全量加载,而是用 chunkById() 分批拉数据,每次用 fputcsv() 写入 php://output 流。
- 必须在
headers中设置Content-Type: text/csv和Content-Disposition: attachment; filename="xxx.csv" - 务必关闭 Laravel 的输出缓冲(
ob_end_clean()),否则 header 会失效或内容重复 - 不要在流回调里做 Eloquent 关联预加载(
with()),分页 chunk 下关联容易错乱
StreamedResponse 类比原生 response()->stream() 更可控
Laravel 9+ 原生支持 Symfony\Component\HttpFoundation\StreamedResponse,它比 response()->stream() 多一层封装,能更明确地控制状态码、header、回调执行时机。
示例核心逻辑:
return new StreamedResponse(function () {
$handle = fopen('php://output', 'w');
// 写表头
fputcsv($handle, ['id', 'name', 'email']);
// 分批查询并写入
User::orderBy('id')->chunkById(500, function ($users) use ($handle) {
foreach ($users as $user) {
fputcsv($handle, [$user->id, $user->name, $user->email]);
}
});
fclose($handle);
}, 200, [
'Content-Type' => 'text/csv',
'Content-Disposition' => 'attachment; filename="users.csv"',
]);
- 注意
chunkById()必须带orderBy字段,且该字段需有索引,否则性能退化严重 - 避免在回调中使用
$this或闭包外的复杂对象,防止内存常驻 - 若需添加 BOM 头(解决 Excel 中文乱码),在
fopen后立即写入\xEF\xBB\xBF
中文字段导出到 Excel 打开乱码?加 BOM 是最稳解法
Excel 默认用 ANSI 解码 CSV,而 PHP 输出是 UTF-8。不加 BOM 时,Windows 版 Excel 极大概率显示为乱码,Mac 版可能正常——这不是编码问题,是 Excel 的解析缺陷。
- 只需在打开文件句柄后、写任何内容前,执行
fwrite($handle, "\xEF\xBB\xBF"); - 不要用
mb_convert_encoding()转 GBK,那会导致其他系统(如 Linux 下 LibreOffice)无法识别 - 如果用第三方库如
spatie/laravel-export,确认其底层是否自动加 BOM;很多默认不加
导出中途报 Connection reset by peer?检查超时与缓冲设置
大导出常见现象:前端卡住、进度条不动、Nginx 返回 502 或直接断连。本质是网关/PHP-FPM 主动切断了长时间连接。
- Nginx 需调大:
proxy_read_timeout 600;、fastcgi_read_timeout 600; - PHP-FPM 需调大:
request_terminate_timeout = 600(慎用,建议优先优化导出逻辑) - Apache 下注意
Timeout和ProxyTimeout设置 - 更稳妥做法:把导出转为队列任务,生成文件后返回下载链接,彻底避开长请求
流式导出看似简单,但实际踩坑集中在 header 顺序、BOM 缺失、分页键无索引、以及网关超时这四点。尤其别信“本地能跑线上就没事”——生产环境的超时链路比开发环境长得多。










