THINKPHP 插入10万数据

本文详细介绍了如何利用THINKPHP框架,优化数据插入操作,高效处理10万条记录到MySQL数据库的过程,包括批量插入、事务处理等关键技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

<?php
namespace app\index\controller;
use think\Controller;
use think\Db;

class Index extends Controller
{
	//测试前开启事务和异常异常捕捉,主动抛异常
	public function index(){
	    date_default_timezone_set("PRC");
	    // 此处修改php.ini的内存,不然无法执行插入,因为数据量太大
	    ini_set('memory_limit','1024M');
	    set_time_limit(0);
	    echo date('Y-m-d H:i:s').'<br>';
	    $start = time();
	    $data = array();
	    for($i=0;$i<100000;$i++){
	        array_push($data,['name'=>'测试','age'=>10]);
	    }
	    print_r(db('test')->insertAll($data));
	    $arr=array('goods_id'=>100);
		print_r(db('test')->where('goods_id>50')->save($arr));//修改数据返回受影响记录数量
	    $end = time();
	    echo '<br>'.date('Y-m-d H:i:s').'<br>';
	    $a = $end-$start;
	    print_r('耗时分秒  '.date('i:s',$a));
	 }
}
<think>我们现有的代码主要问题在于: 1. 虽然使用了生成器逐行读取Excel,但是PhpSpreadsheet在读取时仍然会占用较多内存,特别是对于大文件。 2. 批量插入时使用了模型的saveAll方法,该方法在ThinkPHP中实际上是循环执行单条插入(或者生成多条INSERT语句,但一次执行),但依然不是最高效的方式。 优化方向: 1. 更换更高效的Excel读取库,比如使用box/spout(现在叫openspout/openspout),它采用流式读取,内存占用极低。 2. 使用数据库的批量插入语句,例如使用INSERT INTO ... VALUES (...), (...), ... 的方式,一次插入多条记录。 由于工期紧张,我们尽量在原有代码基础上修改,避免大改。 具体步骤: 第一步:优化Excel读取(如果时间允许,建议更换为openspout,但这里我们先不换,因为原有代码已经是生成器,且PhpSpreadsheet在读取时设置`setReadDataOnly(true)`和`setReadEmptyCells(false)`可以优化) 第二步:优化数据插入,不使用saveAll,而是使用Db::insertAll或者自己拼接SQL(注意防SQL注入)。 在ThinkPHP中,可以使用`Db::name('goods_detail')->insertAll($batchData)`,它会自动将数据拼接成多值插入语句。 但是,`insertAll`方法一次插入数据量是有限制的,因为数据库有max_allowed_packet限制,所以我们需要控制每批插入数据量(比如3000条可能一次插入太多,可以调整为1000条)。 另外,我们还可以考虑使用事务,但注意事务过大也会影响性能,所以可以每批插入提交一次。 由于我们之前没有使用事务,现在也不必要加入。 第三步:调整PHP和服务器配置,确保脚本能够长时间运行。 我们已经在代码开头设置了`set_time_limit(0)`和`ini_set('memory_limit', '2048M')`,但可能还不够。另外,Nginx或Apache的超时设置也需要调整。 但是,502错误通常是由于Nginx反向代理超时,所以我们需要调整Nginx的配置: 在Nginx配置中增加: fastcgi_read_timeout 600s; # 根据实际情况调整,比如10分钟 proxy_read_timeout 600s; 第四步:前端超时设置已经足够长(5小时),所以前端不是问题。 代码修改示例: 1. 修改批量插入的方式,使用`Db::insertAll`(注意:需要确保字段名和数组键名对应) 修改后代码片段: ```php // 替换原来的 saveAll 为 insertAll if (!empty($batchData)) { try { // 使用Db类进行批量插入 Db::name('goods_detail')->insertAll($batchData); $success += count($batchData); } catch (\Exception $e) { $failedBatches[] = [ 'startRow' => $total - count($batchData) + 1, 'endRow' => $total, 'error' => $e->getMessage() ]; } } ``` 注意:`insertAll`方法在ThinkPHP6中默认支持多值插入,但是一次插入数据量不能超过数据库的max_allowed_packet。因此,建议将$batchSize调整为1000或更小,避免插入语句过长。 2. 调整$batchSize为1000。 3. 在Excel读取部分,我们可以尝试优化PhpSpreadsheet的读取性能: - 跳过空行:在生成器中,如果遇到空行,可以跳过。 - 关闭预读:PhpSpreadsheet没有提供关闭预读的选项,所以只能考虑换库。但如果不换,我们只能接受。 4. 如果还是慢,可以考虑在读取Excel时,只读取需要的列,但原有代码已经按列名读取,所以没有问题。 5. 如果可能,将Excel文件转换为CSV格式,然后使用fgetcsv逐行读取,这样速度会快很多。但需要修改上传逻辑。 考虑到工期紧张,我们先不换。 修改后的完整代码(仅修改插入部分和批次大小): ```php public function importGoodsDetail(): Json { set_time_limit(0); ini_set('memory_limit', '2048M'); $params = $this->request->param(); if (empty($params["file"])) { return $this->fail('请上传要导入的文件',[],0,1); } $titleArr = [ "物流履约单号" => "logistics_order_no", "货品名称" => "product_name", "货品ID" => "product_id", "货品件数" => "quantity", "货品重量(克)" => "weight", "货品长度(毫米)" => "length", "货品宽度(毫米)" => "width", "货品高度(毫米)" => "height", "货品体积(立方毫米)" => "volume", ]; try { // 使用生成器获取数据流 $dataGenerator = $this->importExeclGenerator($params['file']); $batchSize = 1000; // 调整为1000条每批 $model = new GoodsDetailModel(); $total = 0; $success = 0; $failedBatches = []; // 记录失败的批次 // 直接处理数据,不要使用事务 $batchData = []; foreach ($dataGenerator as $rowData) { $total++; // 转换数据格式 $item = []; foreach ($rowData as $k => $val) { if ($k && $val && isset($titleArr[$k])) { $item[$titleArr[$k]] = trim($val); } } if (!empty($item)) { $batchData[] = $item; } // 达到批次大小或处理完所有数据插入数据库 if (count($batchData) >= $batchSize) { if (!empty($batchData)) { try { // 使用Db的insertAll方法批量插入 Db::name($model->getName())->insertAll($batchData); $success += count($batchData); } catch (\Exception $e) { // 记录失败批次 $failedBatches[] = [ 'startRow' => $total - count($batchData) + 1, 'endRow' => $total, 'error' => $e->getMessage() ]; } } $batchData = []; // 清空批次数据 } } // 处理剩余数据 if (!empty($batchData)) { try { Db::name($model->getName())->insertAll($batchData); $success += count($batchData); } catch (\Exception $e) { $failedBatches[] = [ 'startRow' => $total - count($batchData) + 1, 'endRow' => $total, 'error' => $e->getMessage() ]; } } // 构建返回结果 $resultMsg = "导入成功,共{$total}条数据,成功导入{$success}条"; if (!empty($failedBatches)) { $resultMsg .= ",失败 ".($total - $success)." 条,具体报错信息:".$e->getMessage(); return $this->fail('导入失败',['remark' => $resultMsg],1,1); } return $this->success('导入成功',['remark' => $resultMsg],1,1); } catch (\Exception $e) { return $this->fail('导入失败',['remark' => $e->getMessage()],1,1); } } ``` 注意:`Db::name($model->getName())` 获取表名,确保正确。 如果表名获取不正确,可以直接写表名,如:`Db::name('goods_detail')`。 另外,在失败记录中,`$e->getMessage()` 在循环外可能未定义,需要修正(上面代码已经修正,在循环内捕获异常,所以$e只在catch块内有效,在构建$resultMsg时,我们使用循环中记录的$failedBatches中的错误信息,但注意最后剩余数据处理时捕获的异常,在构建消息时$e可能未定义,所以需要重构错误消息的构建方式)。 重构错误消息: ```php $failCount = $total - $success; $resultMsg = "导入成功,共{$total}条数据,成功导入{$success}条"; if (!empty($failedBatches)) { $errorMsg = ''; foreach ($failedBatches as $batch) { $errorMsg .= "第{$batch['startRow']}行到{$batch['endRow']}行错误:{$batch['error']};"; } $resultMsg .= ",失败 {$failCount} 条,具体报错信息:".$errorMsg; return $this->fail('导入失败',['remark' => $resultMsg],1,1); } ``` 这样就不会出现未定义变量$e的问题。 如果仍然遇到502超时,请务必检查服务器Nginx和PHP-FPM的超时设置。 最后,如果数据量真的非常大(30万条),建议使用队列异步处理。用户上传文件后,后端返回“正在处理”,然后后端在队列中处理,处理完成后通知用户。 但考虑到工期紧张,先按上述优化。 我们还可以在生成器读取Excel时,跳过完全空的行(避免插入数据),减少插入数据量。 在生成器中,可以这样修改: ```php // 在生成器内部,读取一行后检查是否为空行 $isEmpty = true; foreach ($rowData as $value) { if (!empty($value)) { $isEmpty = false; break; } } if ($isEmpty) { continue; // 跳过空行 } ``` 这样可以减少数据量。 请根据实际情况调整。 由于时间关系,我们只能先做这些优化。 如果还是慢,请考虑更换Excel解析库为openspout,这需要重写生成器函数,但可以大幅提升读取速度和降低内存。 示例使用openspout读取: ```php use Box\Spout\Reader\Common\Creator\ReaderEntityFactory; private function importExeclGenerator(string $file_name): \Generator { $path = app()->getRootPath() . "public/" . $file_name; $reader = ReaderEntityFactory::createXLSXReader(); $reader->open($path); foreach ($reader->getSheetIterator() as $sheet) { foreach ($sheet->getRowIterator() as $rowIndex => $row) { if ($rowIndex === 1) { // 读取标题行,这里假设第一行是标题 $titles = $row->toArray(); continue; } $rowData = $row->toArray(); $item = []; foreach ($titles as $index => $title) { // 假设标题在第一个sheet的第一行 $item[$title] = $rowData[$index] ?? null; } yield $item; } } $reader->close(); } ``` 注意:需要安装openspout/openspout,通过composer: `composer require openspout/openspout` 使用openspout可以极大减少内存占用,加快读取速度。 由于原有代码中标题行是写死的,所以我们需要调整生成器,使其返回的字段名与之前一致(即使用$titleArr映射)。或者,我们可以在生成器内部完成映射,但这样会破坏原有结构。 我们可以这样调整: 在生成器内部,读取第一行标题,然后映射到字段名: ```php private function importExeclGenerator(string $file_name): \Generator { $path = app()->getRootPath() . "public/" . $file_name; $reader = ReaderEntityFactory::createXLSXReader(); $reader->open($path); $titleArr = [ "物流履约单号" => "logistics_order_no", "货品名称" => "product_name", "货品ID" => "product_id", "货品件数" => "quantity", "货品重量(克)" => "weight", "货品长度(毫米)" => "length", "货品宽度(毫米)" => "width", "货品高度(毫米)" => "height", "货品体积(立方毫米)" => "volume", ]; foreach ($reader->getSheetIterator() as $sheet) { foreach ($sheet->getRowIterator() as $rowIndex => $row) { if ($rowIndex === 1) { // 跳过标题行,因为我们有固定的标题映射 continue; } $rowData = $row->toArray(); $item = []; // 注意:这里我们无法知道列的顺序,所以需要根据列标题来映射 // 但是,使用openspout读取时,第一行是列标题,我们可以先读取第一行,然后根据列标题找到对应的列 // 然而,我们这里跳过了第一行,所以需要提前读取第一行获取列的顺序? // 因此,我们需要重新设计:先读取第一行,然后根据第一行的内容确定列索引对应的字段名 // 由于时间关系,这里不展开,建议还是用原来的方式,或者确保Excel列顺序固定,然后按索引映射 // 为了快速实现,我们可以按固定列顺序处理,比如第一列是“物流履约单号”,第二列是“货品名称”...,这样就不需要标题行了 // 但这样不灵活,所以折中方案:在生成器内先读取第一行,然后建立列索引到字段名的映射 } } $reader->close(); } ``` 考虑到时间,我们可能暂时不更换openspout,除非原有方式确实无法满足。 综上所述,优先优化数据插入,调整批次大小,并确保服务器配置正确。 如果优化后仍然超时,请考虑分步处理:先快速将Excel文件拆分成多个小文件,然后分批导入(需要修改前端支持分批上传,或者后端拆分后依次导入),但这改动较大。 最后,如果条件允许,建议使用MySQL的LOAD DATA INFILE命令,该命令可以直接导入CSV文件,速度极快。但需要先将Excel转换为CSV,且需要处理列映射。 以上优化方案请根据实际情况选择。 如果问题仍然存在,请提供更多信息(如服务器环境、PHP版本、数据库表结构等)以便进一步分析。 由于问题描述中要求工期紧张,所以先给出以上快速优化方案。</think>### 现有代码优化方案(基于您的代码) 工期紧张的情况下,我建议在您现有代码基础上进行针对性优化,主要解决以下性能瓶颈: #### 主要问题分析 1. **Excel解析效率低**:`PhpSpreadsheet` 即使使用流式读取,对大文件处理效率仍不高 2. **批量插入性能差**:`saveAll()` 在ThinkPHP中会生成多条INSERT语句 3. **内存使用过高**:每次处理3000条数据时内存峰值过高 4. **超时问题**:PHP脚本执行时间超过Nginx/PHP-FPM默认超时 #### 优化后的代码 ```php public function importGoodsDetail(): Json { set_time_limit(0); ini_set('memory_limit', '2048M'); $params = $this->request->param(); if (empty($params["file"])) { return $this->fail('请上传要导入的文件',[],0,1); } // 立即发送响应头,断开前端连接 if (function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); } $titleArr = [ "物流履约单号" => "logistics_order_no", // ...其他字段映射 ]; try { $batchSize = 2000; // 适当减小批次大小 $total = 0; $success = 0; $batchData = []; $failedRows = []; // 使用更高效的插入方法 $tableName = (new GoodsDetailModel())->getTable(); foreach ($this->importExeclGenerator($params['file']) as $rowIndex => $rowData) { $total++; // 转换数据格式 $item = []; foreach ($rowData as $k => $val) { if ($k && $val && isset($titleArr[$k])) { $item[$titleArr[$k]] = trim($val); } } if (!empty($item)) { $batchData[] = $item; } // 分批处理 if (count($batchData) >= $batchSize) { $result = $this->batchInsert($tableName, $batchData); $success += $result; $batchData = []; // 每批处理后释放内存 gc_collect_cycles(); } } // 处理剩余数据 if (!empty($batchData)) { $result = $this->batchInsert($tableName, $batchData); $success += $result; } // 记录日志代替直接返回 Log::write("导入完成: {$success}/{$total}"); return $this->success('后台导入中', [], 1, 1); } catch (\Exception $e) { Log::error("导入失败: ".$e->getMessage()); return $this->fail('系统处理异常', [], 1, 1); } } /** * 高效批量插入方法 */ private function batchInsert(string $tableName, array $data): int { if (empty($data)) return 0; $fields = implode(',', array_keys($data[0])); $placeholders = rtrim(str_repeat('?,', count($data[0])), ','); $values = []; foreach ($data as $row) { foreach ($row as $value) { $values[] = $value; } } $placeholderGroup = '(' . $placeholders . ')'; $placeholderGroups = implode(',', array_fill(0, count($data), $placeholderGroup)); $sql = "INSERT INTO `{$tableName}` ({$fields}) VALUES {$placeholderGroups}"; try { return Db::execute($sql, $values); } catch (\Exception $e) { Log::error("批量插入失败: ".$e->getMessage()); return 0; } } /** * 优化后的Excel读取方法 */ private function importExeclGenerator(string $file_name): \Generator { $path = app()->getRootPath() . "public/" . $file_name; try { $fileType = IOFactory::identify($path); $reader = IOFactory::createReader($fileType); // 关键优化:分块读取 $reader->setReadDataOnly(true); $reader->setReadEmptyCells(false); // 分块读取设置 $chunkSize = 500; // 每次读取500行 $chunkFilter = new ChunkReadFilter(); $reader->setReadFilter($chunkFilter); $spreadsheet = $reader->load($path); $sheet = $spreadsheet->getSheet(0); // 获取表头 $title = $sheet->rangeToArray('A1:' . $sheet->getHighestColumn() . '1', null, true, true, true)[1]; $highestRow = $sheet->getHighestRow(); $totalChunks = ceil($highestRow / $chunkSize); // 分块读取数据 for ($chunk = 1; $chunk <= $totalChunks; $chunk++) { $startRow = ($chunk == 1) ? 2 : (($chunk - 1) * $chunkSize) + 1; $endRow = min($chunk * $chunkSize, $highestRow); $chunkFilter->setRows($startRow, $endRow); $rows = $sheet->rangeToArray( 'A' . $startRow . ':' . $sheet->getHighestColumn() . $endRow, null, true, true, true ); foreach ($rows as $rowIndex => $rowData) { $item = []; foreach ($rowData as $colIndex => $value) { $colName = $title[$colIndex] ?? null; if ($colName) { $item[$colName] = $value; } } yield $item; } // 清除内存 $sheet->disconnectCells(); unset($rows); gc_collect_cycles(); } $spreadsheet->disconnectWorksheets(); unset($spreadsheet); } catch (\Exception $e) { throw $e; } } // 分块读取过滤器 class ChunkReadFilter implements \PhpOffice\PhpSpreadsheet\Reader\IReadFilter { private $startRow = 0; private $endRow = 0; public function setRows($startRow, $chunkSize) { $this->startRow = $startRow; $this->endRow = $startRow + $chunkSize; } public function readCell($column, $row, $worksheetName = '') { if ($row >= $this->startRow && $row < $this->endRow) { return true; } return false; } } ``` ### 优化点详解 1. **Excel读取优化**: - 使用分块读取过滤器(`ChunkReadFilter`) - 每次只加载500行数据到内存 - 设置 `setReadEmptyCells(false)` 跳过空单元格 - 处理完每个分块后主动释放内存 2. **数据插入优化**: - 使用原生SQL批量插入:`INSERT INTO ... VALUES (...), (...), ...` - 避免使用ORM的`saveAll()`方法 - 每次插入2000条数据(可根据服务器调整) 3. **内存管理优化**: - 使用 `gc_collect_cycles()` 主动触发垃圾回收 - 处理完每个分块后 `unset` 临时变量 - 使用 `disconnectCells()` 和 `disconnectWorksheets()` 释放PhpSpreadsheet内存 4. **响应处理优化**: - 使用 `fastcgi_finish_request()` 立即断开前端连接 - 后台处理完成后记录日志而非直接返回 - 前端轮询导入状态(需额外实现状态查询接口) ### 性能对比估计 | 优化前 | 优化后 | |--------|--------| | 30万条需30+分钟 | 30万条5-8分钟 | | 内存峰值2GB+ | 内存峰值300-500MB | | 高概率502超时 | 避免超时,后台处理 | ### 前端调整建议 ```javascript // 修改提交方法 submitFileForm = async () => { feedback.loading('已开始后台导入,请稍后在日志中查看结果'); try { const response = await importGoodsDetail({ file: fileUrl.value }); // 提示后台处理中 feedback.msgSuccess('导入任务已开始后台处理'); show.value = false; // 可添加状态轮询逻辑 pollImportStatus(); } catch (error) { feedback.msgError("提交失败:" + error.message); } finally { submitLoading.value = false; } } // 轮询状态方法 const pollImportStatus = () => { const checkStatus = async () => { try { const { data } = await getImportStatus(); if (data.completed) { feedback.msgSuccess(`导入完成! 成功:${data.success}条`); } else { setTimeout(checkStatus, 5000); // 5秒后再次检查 } } catch (e) { console.error('状态检查失败', e); } }; setTimeout(checkStatus, 10000); // 10秒后开始检查 } ``` ### 服务器配置调整 在 `php.ini` 中添加: ```ini max_execution_time = 0 memory_limit = 1024M opcache.enable=1 opcache.memory_consumption=256 ``` 在 Nginx 配置中: ```nginx location ~ \.php$ { fastcgi_read_timeout 1800s; # 30分钟 proxy_read_timeout 1800s; } ``` ### 紧急替代方案 如果仍然超时,可考虑: 1. 将Excel拆分为多个小文件分批上传 2. 使用命令行导入(需额外开发) 3. 转换为CSV后用`LOAD DATA INFILE`导入 ```php // CSV快速导入示例 public function importCSV(string $file) { $path = app()->getRootPath() . "public/" . $file; $table = (new GoodsDetailModel())->getTable(); $query = sprintf( "LOAD DATA LOCAL INFILE '%s' INTO TABLE %s FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' LINES TERMINATED BY '\\n' IGNORE 1 LINES (logistics_order_no, product_name, product_id, quantity, weight, length, width, height, volume)", addslashes($path), $table ); Db::execute($query); } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

whaxkl

该打赏续费了

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值