在 Laravel 中导出大量数据,避免内存溢出或者超时问题可以使用流式输出,将数据一行一行地输出到浏览器或者文件中,而不是将所有数据都加载到内存中。Laravel 中可以使用 Chunk 方法来实现流式输出,Chunk 方法将查询结果分块处理,每次处理一定数量的数据
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Response;
use App\Models\User;
public function exportUsers(Request $request)
{
$fileName = 'users.csv';
$headers = [
'Content-type' => 'text/csv',
'Content-Disposition' => sprintf('attachment; filename="%s"', $fileName),
];
$totalRows = User::count();
$chunkSize = 1000;
$totalChunks = ceil($totalRows / $chunkSize);
$stream = fopen('php://output', 'w');
// 写入表头
fputcsv($stream, ['ID', 'Name', 'Email']);
for ($i = 1; $i <= $totalChunks; $i++) {
$offset = ($i - 1) * $chunkSize;
$users = User::offset($offset)->limit($chunkSize)->get();
foreach ($users as $user) {
// 写入一行数据
fputcsv($stream, [$user->id, $user->name, $user->email]);
}
// 强制刷新输出缓冲区
ob_flush();
flush();
}
fclose($stream);
return Response::make('', 200, $headers);
}
在这个示例代码中,我们使用 php://output 创建一个流,将数据输出到这个流中。这样可以避免将所有数据读入内存中,降低内存占用。
我们通过循环分块读取数据,每次读取 $chunkSize 条数据,然后将这些数据写入到流中。在写入数据后,我们使用 ob_flush() 和 flush() 强制刷新输出缓冲区,将已经写入的数据发送到客户端,避免输出缓冲区占用过多内存。
最后,我们通过 Response 返回一个空内容的响应,同时设置相应的 Content-Type 和 Content-Disposition 头,告诉浏览器下载这个 CSV 文件。
为了避免超时和内存溢出问题,我们可以在 php.ini 中设置 max_execution_time 和 memory_limit。如果需要在代码中设置这些限制,可以使用 set_time_limit() 和 ini_set() 函数。
这是我写的一段导出百万数据的测试代码,其整个运行时间40秒左右,这个xslwriter真是厉害哈
ini_set('memory_limit', '1024M');
set_time_limit(0);
$path = \think\facade\App::getRootPath() . 'public/upload/';
$config = [
'path' => $path,
];
$fileName = 'tutorial01.xlsx';
$excel = new \Vtiful\Kernel\Excel($config);
$fileObject = $excel->constMemory($fileName, NULL, false);
$fileHandle = $fileObject->getHandle();
$format = new \Vtiful\Kernel\Format($fileHandle);
$boldStyle = $format->bold()->toResource();
$fileObject = $fileObject->setRow('A1', 10, $boldStyle) // 写入数据前设置行样式
->header(['id', 'person_id', 'person_name', 'gmt_create', 'gmt_modified']);
for ($i = 5000; $i <= 1000000; $i += 5000) {
$offset = $i - 5000;
$limit = 5000;
$data = Db::table('t1')->limit($offset, $limit)->select();
$data = collect($data)->map(function ($item) {
return collect($item)->values();
})->toArray();
$fileObject = $fileObject->data($data);
}
$filePath = $excel->output();
header("Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
header('Content-Disposition: attachment;filename="' . $fileName . '"');
header('Content-Length: ' . filesize($filePath));
header('Content-Transfer-Encoding: binary');
header('Cache-Control: must-revalidate');
header('Cache-Control: max-age=0');
header('Pragma: public');
ob_clean();
flush();
if (copy($filePath, 'php://output') === false) {
exit('copy file to path error');
}
@unlink($filePath);
我是这样写的,分批查询,边查询边输出的,截取一段代码:
set_time_limit(0);
ini_set('memory_limit', '128M');
$fileName = "零钱提现".date('YmdHis', time());
header('Content-Type: application/vnd.ms-execl');
header('Content-Disposition: attachment;filename="' . $fileName . '.csv"');
$begin = microtime(true);
//打开php标准输出流
//以写入追加的方式打开
$fp = fopen('php://output', 'a');
//我们试着用fputcsv从数据库中导出1百万的数据
//我们每次取1万条数据,分100步来执行
//如果线上环境无法支持一次性读取1万条数据,可把$nums调小,$step相应增大。
$total = self::$model->getTotal($startDate, $endDate);
$nums = 10000;
$totalPage = ceil($total / $nums);
//设置标题
$title = [
'recordID',
'playerID',
'showID',
'玩家昵称',
'提现金额(元)',
'提现后零钱(元)',
'时间',
'状态',
];
//将标题写到标准输出中
fputcsv($fp, $title);
for ($page = 1; $page <= $totalPage; ++$page) {
//分页取
$data = self::$model->getList($startDate, $endDate, $nums);
//先取出玩家id
$playerIds = array_unique(array_column($data, 'playerID'));
//获取玩家信息
$user = Obj::model('playerBasicData')->getUserInfoByAttr($playerIds);
if ($data) {
foreach ($data as $row) {
$temp = [
//recordID
$row['recordID'] . "\t",
//playerID
$row['playerID'] . "\t",
//showID
$user[$row['playerID']]['showID'] . "\t",
//昵称
$user[$row['playerID']]['nickName'] . "\t",
//'提现金额(元)'
($row['amount'] / 100),
//'提现后零钱(元)'
($row['changeAfter'] / 100),
//'时间'
$row['createTime'] . "\t",
//'状态'
Model_My_WithdrawRecord::STATUS_TEXT[$row['status']] . "\t"
];
fputcsv($fp, $temp);
}
//每1万条数据就刷新缓冲区
ob_flush();
flush();
}
}
我是这么写的,贴个代码参考一下,当时测试20几秒
set_time_limit(0);
ini_set('memory_limit', '128M');
$fileName = "零钱提现".date('YmdHis', time());
header('Content-Type: application/vnd.ms-execl');
header('Content-Disposition: attachment;filename="' . $fileName . '.csv"');
$begin = microtime(true);
//打开php标准输出流
//以写入追加的方式打开
$fp = fopen('php://output', 'a');
//我们试着用fputcsv从数据库中导出1百万的数据
//我们每次取1万条数据,分100步来执行
//如果线上环境无法支持一次性读取1万条数据,可把$nums调小,$step相应增大。
$total = self::$model->getTotal($startDate, $endDate);
$nums = 10000;
$totalPage = ceil($total / $nums);
//设置标题
$title = [
'recordID',
'playerID',
'showID',
'玩家昵称',
'提现金额(元)',
'提现后零钱(元)',
'时间',
'状态',
];
//将标题写到标准输出中
fputcsv($fp, $title);
for ($page = 1; $page <= $totalPage; ++$page) {
//分页取
$data = self::$model->getList($startDate, $endDate, $nums);
//先取出玩家id
$playerIds = array_unique(array_column($data, 'playerID'));
//获取玩家信息
$user = Obj::model('playerBasicData')->getUserInfoByAttr($playerIds);
if ($data) {
foreach ($data as $row) {
$temp = [
//recordID
$row['recordID'] . "\t",
//playerID
$row['playerID'] . "\t",
//showID
$user[$row['playerID']]['showID'] . "\t",
//昵称
$user[$row['playerID']]['nickName'] . "\t",
//'提现金额(元)'
($row['amount'] / 100),
//'提现后零钱(元)'
($row['changeAfter'] / 100),
//'时间'
$row['createTime'] . "\t",
//'状态'
Model_My_WithdrawRecord::STATUS_TEXT[$row['status']] . "\t"
];
fputcsv($fp, $temp);
}
//每1万条数据就刷新缓冲区
ob_flush();
flush();
}
}
我这样写的
set_time_limit(0);
ini_set('memory_limit', '128M');
$fileName = "零钱提现".date('YmdHis', time());
header('Content-Type: application/vnd.ms-execl');
header('Content-Disposition: attachment;filename="' . $fileName . '.csv"');
$begin = microtime(true);
//打开php标准输出流
//以写入追加的方式打开
$fp = fopen('php://output', 'a');
//我们试着用fputcsv从数据库中导出1百万的数据
//我们每次取1万条数据,分100步来执行
//如果线上环境无法支持一次性读取1万条数据,可把$nums调小,$step相应增大。
$total = self::$model->getTotal($startDate, $endDate);
$nums = 10000;
$totalPage = ceil($total / $nums);
//设置标题
$title = [
'recordID',
'playerID',
'showID',
'玩家昵称',
'提现金额(元)',
'提现后零钱(元)',
'时间',
'状态',
];
//将标题写到标准输出中
fputcsv($fp, $title);
for ($page = 1; $page <= $totalPage; ++$page) {
//分页取
$data = self::$model->getList($startDate, $endDate, $nums);
//先取出玩家id
$playerIds = array_unique(array_column($data, 'playerID'));
//获取玩家信息
$user = Obj::model('playerBasicData')->getUserInfoByAttr($playerIds);
if ($data) {
foreach ($data as $row) {
$temp = [
//recordID
$row['recordID'] . "\t",
//playerID
$row['playerID'] . "\t",
//showID
$user[$row['playerID']]['showID'] . "\t",
//昵称
$user[$row['playerID']]['nickName'] . "\t",
//'提现金额(元)'
($row['amount'] / 100),
//'提现后零钱(元)'
($row['changeAfter'] / 100),
//'时间'
$row['createTime'] . "\t",
//'状态'
Model_My_WithdrawRecord::STATUS_TEXT[$row['status']] . "\t"
];
fputcsv($fp, $temp);
}
//每1万条数据就刷新缓冲区
ob_flush();
flush();
}
}
导出excel内存超了主要就是因为执行期间,整个excel都放到内存里,这样子数据越多就越卡
我这边总结了三个方法
- 离线异步生成excel,然后前端只需要直接下载excel文件就不会有这个问题
- 使用csv,分批查询,一批一批的输出到前端
- 选一个可以分批处理的excel扩展,这个扩展是支持分批处理,解决csv样式问题
推荐文章: