Laravel高效导入万级以上CSV/XLS至数据库的技术咨询
解决Laravel批量CSV/XLS导入的性能与超时问题
嘿,咱来一步步搞定你遇到的这三个批量导入痛点:
1. 如何快速导入≥10000条记录的CSV/XLS至数据库?
你当前的代码是逐条创建模型并调用save(),这是效率最低的方式——每条记录都会触发一次独立的数据库查询,还会跑Laravel的模型事件(比如creating/created),1万条下来自然慢得离谱。推荐这几个优化方向:
方案一:用Laravel批量插入(性能提升最明显)
把所有数据整理成数组,一次性插入数据库,全程只生成1条SQL语句(或分块生成少量语句),速度能提升几十倍:
public function importDataSet(Request $request) { if($request->hasFile('file')){ $file = $request->file('file'); $uname = uniqid(); $oname = $file->getClientOriginalName(); $filename = $uname.$oname; $filelocation = 'data-sets'; $filepath = $file->move($filelocation, $filename); ini_set('auto_detect_line_endings', TRUE); $rows = array_map('str_getcsv', file($filepath)); array_shift($rows); // 移除表头 // 整理批量插入的数据集 $transactions = []; foreach ($rows as $row) { $transactions[] = [ 'dataset_id' => 1, 'itemsets' => json_encode($row), 'created_at' => now(), 'updated_at' => now() ]; } // 分块插入(避免SQL语句过长,每次插1000条) collect($transactions)->chunk(1000)->each(function($chunk) { Transaction::insert($chunk->toArray()); }); } return '导入完成!'; }
⚠️ 注意:批量插入不会触发模型的save()方法,所以模型事件不会执行,如果你的业务依赖这些事件,需要手动补充逻辑。
方案二:用第三方Excel处理包(推荐,更省心)
比如Maatwebsite/Laravel-Excel,它内置了分块读取、自动处理表头、批量插入的功能,还支持XLS/XLSX格式,代码更简洁:
// 先安装包:composer require maatwebsite/excel use Maatwebsite\Excel\Facades\Excel; public function importDataSet(Request $request) { if($request->hasFile('file')){ Excel::import(new TransactionImport, $request->file('file')); return '导入完成!'; } return '请选择要导入的文件'; } // 创建Import类:app/Imports/TransactionImport.php namespace App\Imports; use App\Models\Transaction; use Maatwebsite\Excel\Concerns\ToModel; use Maatwebsite\Excel\Concerns\WithChunkReading; class TransactionImport implements ToModel, WithChunkReading { public function model(array $row) { return new Transaction([ 'dataset_id' => 1, 'itemsets' => json_encode($row) ]); } // 每次读取1000条,避免内存溢出 public function chunkSize(): int { return 1000; } }
方案三:临时关闭模型事件(如果必须用save())
如果你业务上需要保留save()逻辑,可以临时关闭模型的事件分发,减少额外开销:
// 临时关闭事件 Transaction::unsetEventDispatcher(); foreach ($rows as $row) { $transaction = new Transaction; $transaction->dataset_id = 1; $transaction->itemsets=json_encode($row); $transaction->save(); } // 恢复事件分发 Transaction::setEventDispatcher(app('events'));
2. 使用set_time_limit(0)是否合理?有无更优方案?
set_time_limit(0)虽然能绕开PHP的超时限制,但非常不推荐,原因有两个:
- 它会长时间占用Web服务器的进程,11分钟的请求会让服务器资源被占死,并发高的时候很容易崩溃;
- 就算PHP不超时,Web服务器(比如Nginx、Apache)也有自己的超时时间(比如Nginx默认60秒),还是会返回504错误。
更优方案:用队列异步处理
把导入任务放到Laravel队列里,用户上传文件后直接返回成功,后台队列进程慢慢处理导入,体验和性能都拉满:
// 第一步:生成队列任务类 php artisan make:job ImportTransactionJob // app/Jobs/ImportTransactionJob.php namespace App\Jobs; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use App\Models\Transaction; class ImportTransactionJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; protected $filepath; public function __construct($filepath) { $this->filepath = $filepath; } public function handle() { ini_set('auto_detect_line_endings', TRUE); $rows = array_map('str_getcsv', file($this->filepath)); array_shift($rows); $transactions = []; foreach ($rows as $row) { $transactions[] = [ 'dataset_id' => 1, 'itemsets' => json_encode($row), 'created_at' => now(), 'updated_at' => now() ]; } collect($transactions)->chunk(1000)->each(function($chunk) { Transaction::insert($chunk->toArray()); }); // 导入完成后删除临时文件 unlink($this->filepath); } } // 第二步:控制器里分发任务 public function importDataSet(Request $request) { if($request->hasFile('file')){ $file = $request->file('file'); $uname = uniqid(); $oname = $file->getClientOriginalName(); $filename = $uname.$oname; $filelocation = 'data-sets'; $filepath = $file->move($filelocation, $filename); // 把任务丢进队列 ImportTransactionJob::dispatch($filepath); return '导入任务已提交,后台处理中,请稍后查看结果!'; } return '请选择要导入的文件'; }
最后启动队列 worker:php artisan queue:work,导入任务就会在后台异步执行,用户不用傻等,服务器也不会被卡爆。
3. 能否在执行插入前预估所需最长时间?
可以做近似预估,原理是先测试一小部分数据的插入时间,再推算总时长。示例代码:
public function estimateImportTime(Request $request) { if($request->hasFile('file')){ $file = $request->file('file'); $filepath = $file->store('temp'); ini_set('auto_detect_line_endings', TRUE); $rows = array_map('str_getcsv', file($filepath)); array_shift($rows); $totalRows = count($rows); // 取前100条测试插入耗时 $testRows = array_slice($rows, 0, 100); $start = microtime(true); $transactions = []; foreach ($testRows as $row) { $transactions[] = [ 'dataset_id' => 1, 'itemsets' => json_encode($row), 'created_at' => now(), 'updated_at' => now() ]; } Transaction::insert($transactions); $testTime = microtime(true) - $start; $estimatedTime = ($testTime / 100) * $totalRows; // 转换为易读的分秒格式 $minutes = floor($estimatedTime / 60); $seconds = floor($estimatedTime % 60); return "预估导入时间:{$minutes}分{$seconds}秒(实际时长受服务器负载影响)"; } }
⚠️ 注意:这只是近似值,实际时间会受服务器CPU、数据库连接数、磁盘IO等因素影响,最好给用户一个范围(比如“预估5-10分钟”),而不是精确值。
内容的提问来源于stack exchange,提问作者Ezehlivinus
相关产品推荐
相关产品推荐

