大批量INSERT卡死或超时是因长事务持续持有锁、undo/redo log膨胀及日志刷盘压力大;应分片为1000–5000行/批,每批1–5秒内提交,控制总字节数低于innodb_log_file_size的30%。

为什么大批量 INSERT 会卡死或超时
MySQL 默认在事务中执行大批量 INSERT 时,会持续持有行锁(或表锁,取决于存储引擎和隔离级别),同时 undo log 和 redo log 持续增长。InnoDB 在单事务中写入超过几万行就可能触发 Lock wait timeout exceeded 或 MySQL server has gone away —— 这不是网络问题,而是事务太长、锁未释放、日志刷盘压力大导致的。
用循环分片控制事务大小的关键参数
核心是把 100 万行拆成多个小事务,每批提交一次。重点不是“分多少次”,而是让每批能在 1–5 秒内完成提交,避免锁竞争和主从延迟激增。
-
autocommit=0必须关闭,否则每次INSERT都自动提交,失去分片意义 - 每批行数建议设为
1000–5000:太小(如 100)会增加网络往返和事务开销;太大(如 20000)仍可能触碰innodb_log_file_size限制或锁等待 - 显式调用
COMMIT后立即BEGIN(或START TRANSACTION),不要依赖隐式开启 - 如果用 LOAD DATA INFILE,它本身不支持分片,必须先切文件,再循环执行多条
LOAD DATA INFILE
Python + PyMySQL 示例:带异常回滚的分片插入
注意不是简单 for 循环套 execute,要捕获异常并确保事务原子性:
conn.autocommit = False
cursor = conn.cursor()
<p>batch_size = 2000
for i in range(0, len(data_rows), batch_size):
batch = data_rows[i:i + batch_size]
try:
cursor.executemany("INSERT INTO t_user (name, email) VALUES (%s, %s)", batch)
conn.commit() # 成功才提交
except Exception as e:
conn.rollback()
print(f"batch {i} failed: {e}")
raise # 或记录后继续下一批,视业务容忍度而定</p>⚠️ 容易踩的坑:executemany 不保证原子性——某一行失败时,前面成功的行已写入(除非用 INSERT ... ON DUPLICATE KEY UPDATE 或全量事务包裹);所以更安全的做法是手动拼 INSERT INTO ... VALUES (...), (...), (...) 单条语句,再用 execute 执行。
InnoDB 日志与分片大小的实际约束
分片不能只看行数,还要看单批数据总字节数。因为 innodb_log_file_size 限制了单次事务能写入 redo log 的上限(通常默认 48MB)。如果一批插入包含大量 TEXT / BLOB 字段,2000 行也可能超限,报错 MySQL: Got error 1205 'Deadlock found when trying to get lock' 或直接 crash。
- 用
SELECT SUM(LENGTH(email) + LENGTH(name)) FROM ...估算单行平均体积 - 确保
batch_size × avg_row_bytes < innodb_log_file_size × 0.3(留足缓冲) - 线上调大
innodb_log_file_size需重启,且不能 >512MB(MySQL 5.7+ 允许更大,但恢复时间显著增加)
真正卡住的时候,往往不是语法或连接问题,而是 redo log 写满触发 checkpoint 阻塞,或 long transaction 导致 purge 线程跟不上——这些在 slow log 里看不到,得查 SHOW ENGINE INNODB STATUS 里的 LOG 和 TRANSACTIONS 部分。










