国内海外服务器测评及优惠
Linux服务器运维救灾服务

INSERT INTO ... SELECT 从大表复制数据的锁与日志优化

不会锁住整个源表,但会加临键锁;若无索引或WHERE不走索引,则等效“准全表锁”;需分批处理、禁用autocommit并手动提交以避免长事务与binlog膨胀。

大表 INSERT INTO … SELECT 会锁住源表吗?

MySQL 默认在可重复读(RR)隔离级别下,INSERT INTO ... SELECT 对源表加的是临键锁(Next-Key Lock),不是表锁,但实际效果可能接近全表扫描级锁定——尤其当源表没有合适索引、或 WHERE 条件无法走索引时,InnoDB 会对扫描到的每条记录及其间隙加锁。这意味着并发 SELECT 或 UPDATE 可能被阻塞。

  • 如果源表有主键或唯一索引,且 SELECT 中的 WHERE 能命中索引范围,锁只会落在匹配的索引区间上
  • 如果没走索引(例如 WHERE status LIKE '%draft%'),InnoDB 会遍历聚簇索引,对所有扫描行加锁,等效于“准全表锁”
  • 在 MySQL 8.0+ 中,若启用了 innodb_locks_unsafe_for_binlog=OFF(默认),该语句仍遵循 RR 的加锁逻辑;设为 ON 可降级为记录锁,但会破坏 binlog 一致性,不推荐

如何避免长事务与 binlog 爆炸?

INSERT INTO ... SELECT 是一个原子语句,整个操作记为单条 binlog event。如果复制百万行,这条 event 就会非常大,导致:

  • 主从延迟加剧(从库需重放巨型 event)
  • max_binlog_size 失效,单个 binlog 文件可能远超设定值
  • 若中途失败,事务回滚代价高,undo log 占用猛增

解决思路是拆分 + 控制事务粒度:

  • WHERE id BETWEEN ? AND ? 分批次拉取,配合自增主键切片(如每次 10 万行)
  • 每批单独事务提交,避免长事务拖慢 MVCC 清理
  • 设置 innodb_log_file_size 足够大(建议 ≥2GB),防止频繁 checkpoint
  • 关键:禁用 autocommit 后手动 COMMIT,不要依赖隐式提交

SELECT 部分能否加 LOCK IN SHARE MODE 或 FOR UPDATE?

不能,也不该加。

一站式AI应用开发和部署工具

  • INSERT INTO ... SELECT 语法中,SELECT 子句不允许出现 FOR UPDATELOCK IN SHARE MODE,MySQL 会直接报错 ERROR 1205 (HY000): Deadlock found when trying to get lock 或更明确的语法错误
  • 即使绕过语法限制(比如先 SELECT ... INTO @varINSERT),也会失去原子性,且手动加锁容易引发死锁
  • 真实需求往往是“防止源数据被改”,这时应确保业务层已规避写入冲突,或改用应用层分页 + 应用控制版本号/时间戳校验

替代方案:用 LOAD DATA 或 pt-archiver 更稳?

LOAD DATA INFILEpt-archiver 是更可控的选择,尤其对超大表:

  • LOAD DATA 不经过 SQL 解析层,速度更快,锁等待更短;但需先导出为文本,且目标表不能有外键或触发器干扰
  • pt-archiver 默认按主键分块、逐批 DELETE + INSERT,支持限流(--limit)、休眠(--sleep)、错误跳过,对线上影响小
  • 注意:pt-archiver--bulk-insert 模式本质仍是多行 INSERT,不会变成 INSERT ... SELECT,所以不触发源表临键锁

真正容易被忽略的是:即使加了索引、分了批、关了 autocommit,只要源表存在未提交的长事务(比如另一个连接正在跑 UPDATE 无 WHERE),INSERT INTO ... SELECT 仍可能因 read view 太旧而卡住——这不是锁问题,是 MVCC 快照可见性导致的隐式等待。

赞(0) 打赏
未经允许不得转载:linuxcto运维 » INSERT INTO ... SELECT 从大表复制数据的锁与日志优化

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续提供更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫

微信扫一扫