根本不是数据太多,而是默认加载方式和写入姿势错了:saveAll()传几万条、insertAll()不分片、验证全开、事务包百万数据,三者叠加致512MB内存崩溃;应改用Db::insertAll()分批200–500条、禁用debug、流式读Spout、每批事务+gc回收。

ThinkPHP批量插入内存溢出,根本不是数据太多,而是默认加载方式和写入姿势错了——saveAll() 传入几万条数据、Db::insertAll() 不分片、模型验证全开、事务包着整个百万数组,这三样凑一起,512MB 内存都扛不住。
用 Db::insertAll() 而不是 saveAll() 做纯写入
saveAll() 走模型层,会触发验证、事件、自动时间戳、类型转换、钩子函数,每条记录都做一次对象实例化;Db::insertAll() 只拼 SQL、只执行,轻量十倍以上。
- 必须传二维数组,字段名严格匹配数据库列名(
create_time这类不会自动填充,得自己塞进去) - 不支持
json字段自动序列化,提前json_encode() - 返回值是影响行数(
int),不是 ID 列表,别指望靠它回填主键 - 高并发下锁表风险更低,因为不读模型配置、不初始化验证器
必须分片,且每批控制在 200–500 条
MySQL 的 max_allowed_packet 默认才 4MB,拼太长的 INSERT INTO ... VALUES (),(),()... 直接报 Packets larger than max_allowed_packet are not allowed。
- 用
array_chunk($data, 400)切片,别依赖框架自动分 - CLI 脚本里每批后加
gc_collect_cycles(),防止引用堆积 - Web 请求别硬扛,改用队列或前端分片上传,否则 Nginx 30 秒就断连
- 别在循环里反复
new User(),复用一个模型实例也没用——Db::insertAll()根本不走模型
事务包裹 + 异常捕获是强制项
不分事务的批量插入,失败时只能部分成功,数据一致性彻底失控。但事务不能包全部,否则锁表太久、日志暴涨、内存更悬。
立即学习“PHP免费学习笔记(深入)”;
- 用
Db::transaction(function () use ($chunk) { Db::insertAll($chunk); }),每批一个事务 - 千万别混用
saveAll()和Db::insertAll()在同一个事务里——它们底层可能用不同 PDO 连接,事务不生效 - 事务内禁用日志:确保
app_debug = false,删掉所有Log::debug($hugeData)或dump() - 如果中间表要记日志,每处理 10 批(即 4000 行)清一次:
Db::execute('TRUNCATE TABLE `import_tmp_log`')
Excel 导入百万行?绕开框架解析,直接上 Spout 流式读
PhpSpreadsheet 加载一个 50 万行 .xlsx,内存轻松破 2GB;Spout 恒定 3~5MB,且不建 DOM、不解压全文件。
- 装
composer require box/spout:^4.0,确认php_zip扩展已启用(Windows 用户检查php.ini里extension=zip是否开启) - Spout 不支持
.xls,前端必须校验后缀为.xlsx或.csv,否则静默失败 - 逐行读,每 1000 行攒成一个
$batch,调Db::insertAll($batch)写入,立刻unset($batch) - CLI 运行:加
php -d memory_limit=512M think import:excel,别信 Web 环境的 php.ini
最易被忽略的一点:所有这些优化,前提是你关掉了 app_debug。开着调试模式跑批量,SQL 日志、模板编译缓存、错误堆栈全驻留内存,再好的分片也救不回来。



















