根本原因是单事务持有锁时间过长导致等待超时;需显式BEGIN/COMMIT分批,每批1000–5000行,避免隐式提交、锁冲突及连接状态残留。

为什么大批量 INSERT 会触发 Lock wait timeout exceeded
根本原因不是数据量大,而是单事务持有行锁/表锁时间过长,其他事务排队等待超时。MySQL 默认 innodb_lock_wait_timeout 是 50 秒,一旦某条 INSERT 事务卡住(比如索引冲突、磁盘慢、主从延迟引发的 MDL 锁等),后续所有依赖同一资源的语句都会在 50 秒后抛出这个错误。
分批插入必须控制事务粒度,不是简单切分数组
常见误区是把 10 万条数据按每 1000 条一组循环,但没显式控制事务——结果还是每个 INSERT 都自动开启独立小事务,反而放大了日志写入和锁竞争开销。
- 务必用
BEGIN/COMMIT显式包裹每一批,例如:先BEGIN,再批量执行INSERT INTO ... VALUES (...),(...),...(单条语句含多行值),最后COMMIT - 每批行数建议 1000–5000,取决于单行平均长度和
innodb_log_file_size;超过 1 万容易触发innodb_log_file_size不足导致刷盘阻塞 - 避免在循环内反复
COMMIT后立刻BEGIN,中间加个usleep(1000)(PHP)或time.Sleep(time.Millisecond)(Go)能缓解 CPU 抢占和锁队列堆积
INSERT ... ON DUPLICATE KEY UPDATE 分批时要小心唯一键判断逻辑
如果用该语法做“存在则更新、不存在则插入”,分批提交不会改变语义,但要注意:唯一键冲突检测发生在语句执行期,不是事务提交期。也就是说,同一批里的两条记录若互撞唯一键(比如都试图插入相同 email),会直接报错,而不是等到提交才失败。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- 确保每批内部无重复唯一键值,可在应用层用
array_unique($rows, SORT_REGULAR)(PHP)或map去重 - 不要依赖数据库去重来“兜底”,否则分批失去意义,且错误位置难以定位
- 如果业务允许覆盖,优先用
REPLACE INTO,它本质是 DELETE + INSERT,在主键/唯一键冲突时行为更可预测
别忽略 autocommit 和连接状态的隐式影响
很多 ORM 或连接池默认开启 autocommit=1,此时即使写了 BEGIN,下一条非 DML 语句(如 SELECT、SET)也可能意外触发自动提交,让事务提前结束。
- 执行前先确认连接状态:
SELECT @@autocommit;,必要时显式设为 0:SET autocommit = 0; - 避免在事务中混用
SELECT ... FOR UPDATE或SELECT ... LOCK IN SHARE MODE,它们会延长锁持有时间,抵消分批效果 - 使用连接池时,注意某些池(如 MySQLi 的 persistent 连接)可能复用上次未清理的事务状态,每次分批前加
ROLLBACK更稳妥
分批本身不难,难的是每一批的边界是否干净、锁是否真正释放、连接状态是否可控——这些细节一漏,就又回到锁超时的老路。










