PHP 8.3+ 事务导入核心是预处理+手动beginTransaction()+分批提交;PDO设AUTOCOMMIT=false不等于开启事务,必须显式beginTransaction(),且需防重复开启、异常回滚、BOM/空行处理及连接存活检测。

PHP 8.3+ 环境下,用事务导入数据的核心不是“版本新”,而是预处理 + 手动事务控制 + 分批提交——PDO 默认已关闭自动提交,但你仍必须显式调用 beginTransaction(),否则 commit() 无效。
为什么 PDO::ATTR_AUTOCOMMIT => false 不等于自动开启事务
很多人误以为设了 PDO::ATTR_AUTOCOMMIT => false 就万事大吉,其实这只是关掉了每条语句自动提交的开关,**事务本身仍需手动开启**。不调用 beginTransaction(),所有 INSERT 都在隐式事务里各自提交,rollback() 无法回滚任何内容。
- 必须在插入前明确执行
$pdo->beginTransaction() - 若连接已处于活动事务中(比如上一次没 commit 或 rollback),再调用
beginTransaction()会抛出PDOException: There is already an active transaction - 推荐加一层保护:
if (!$pdo->inTransaction()) { $pdo->beginTransaction(); }
prepare() 绑定参数后,execute() 失败不会自动 rollback
预处理语句执行失败(如字段类型不匹配、NOT NULL 字段传了 null),只抛异常,不会触发事务自动回滚。你得自己 catch 并调用 rollback(),否则后续 commit() 会把前面成功的部分也提交出去。
- 务必用
try ... catch包裹整个插入循环 - catch 中先
error_log()记录错误行和$stmt->errorInfo(),再$pdo->rollback() - 不要在循环里 catch 后 continue —— 这会让事务“半成功”,破坏一致性
分批大小选 500 还是 1000?看的是 MySQL 的 max_allowed_packet
拼多值 INSERT(INSERT INTO t(a,b) VALUES (?,?),(?,?)...)时,单条 SQL 长度受 MySQL 的 max_allowed_packet 限制(默认 4MB)。500 行 vs 1000 行,实际影响的是这条 SQL 字符串长度,而非 PHP 内存占用。
立即学习“PHP免费学习笔记(深入)”;
- 用
strlen($sql)粗略估算:假设每行平均 200 字节,500 行 ≈ 100KB,远低于默认值;1000 行 ≈ 200KB,依然安全 - 真正卡住的往往是含长文本字段(如 description TEXT)的 CSV 行——某一行就超 1MB,整批就失败
- 建议初始设为 500,上线后根据日志里的
MySQL server has gone away错误动态下调
fgetcsv() 读 CSV 时,空行和 BOM 头容易导致字段数错乱
fgetcsv() 对 UTF-8 BOM(\xEF\xBB\xBF)不敏感,首行若带 BOM,str_getcsv() 可能解析出错;空行返回 false 或空数组,若没过滤,插入时字段数对不上,直接报错 Column count doesn't match value count。
- 打开文件后立即跳过 BOM:
fseek($handle, 3, SEEK_CUR)(仅当检测到开头是\xEF\xBB\xBF) - 每次
fgetcsv()后检查:if (empty($row) || $row === [null]) { continue; } - 校验字段数:
if (count($row) !== 5) { error_log("行 {$lineNum} 字段数异常: " . json_encode($row)); continue; }
事务导入最易被忽略的点:**没验证 MySQL 连接是否还活着**。长耗时导入(>30 秒)可能触发 wait_timeout,后续 execute() 报 MySQL server has gone away,但 PDO 默认不重连。得在循环里加 if (!$pdo->getAttribute(PDO::ATTR_CONNECTION_STATUS)) { /* 重建连接 + 重新 beginTransaction */ }——这点连很多框架封装都漏了。



















