关闭autocommit并用begin/commit包裹批量insert,可将每行一次的刷盘、加锁、写日志开销降至每批一次,实测1万行插入从30+秒降至约300ms;但单事务不宜超5000行,需防undo膨胀、锁超时与主从延迟。

因为默认 autocommit=1,每条 INSERT 都是独立事务,强制刷盘、写日志、加锁、释放锁——批量插入时,把这些开销从“每行一次”压到“每批一次”,I/O 和锁竞争直接降维打击。
autocommit=1 下单条 INSERT 到底干了啥
你以为只是插一行?MySQL 实际上在背后跑了完整事务生命周期:
-
INSERT触发隐式BEGIN - 写
redo log(内存缓冲) - 根据
innodb_flush_log_at_trx_commit值决定是否立即刷盘(默认=1,每次必刷) - 更新聚簇索引和所有二级索引(可能触发页分裂)
- 写
binlog(如果开启) - 释放行锁 / 表锁
- 提交并清空事务上下文
1 万次?就是 1 万次磁盘 I/O、1 万次锁管理、1 万次日志序列化。慢不是 CPU 不够,是硬盘在喊累。
显式事务怎么省掉这些重复动作
用 BEGIN + 多条 INSERT + COMMIT 后,关键变化有三处:
-
redo log只在COMMIT时刷一次盘(前提是innodb_flush_log_at_trx_commit=1;设为 2 可进一步提速,但断电可能丢 1 秒数据) - 索引更新延迟到事务结束前批量合并,避免 B+ 树反复分裂
- 行锁持有时间从“每行毫秒级”拉长为“整批执行时间”,锁获取/释放次数从 N 次降到 1 次
实测中,插入 1 万行,耗时通常从 30+ 秒降到 300ms 左右——不是优化,是绕开了设计上的冗余路径。
为什么不能把全部数据塞进一个事务
事务越长,风险越高,不是越“大”越好:
-
undo log持续膨胀,可能撑爆ibdata1或触发频繁 checkpoint - 主从复制延迟飙升,尤其 binlog 是按事务粒度写入的
-
innodb_lock_wait_timeout容易超时,其他会话被阻塞卡死 - OOM 风险:大事务需要更多内存维护事务状态和回滚段
推荐单次事务控制在 1000~5000 行之间。具体看单行大小、max_allowed_packet 设置(必须 ≥ 批量 SQL 字节数),以及服务器内存余量。超过 5000 行,拆。
应用层写法里最容易漏掉的细节
光写 BEGIN 和 COMMIT 不够,连接池环境下极易出问题:
- Python 的
pymysql默认autocommit=True,得先设conn.autocommit = False - Java 的 JDBC 需调用
connection.setAutoCommit(false),否则BEGIN被忽略 - ORM 如 MyBatis,优先用
@Transactional,别裸写 SQL 控制事务边界 - 必须配对
COMMIT或ROLLBACK,否则连接复用时会卡在未提交事务里,后续查询全被锁住
真正卡住人的,往往不是“要不要用事务”,而是“用完没清理干净”。事务边界一旦失控,比不用还麻烦。











