大事务必然触发锁等待和主从延迟,根治方式是控制事务体积、边界清晰、及时提交;update/delete大表卡住因row格式下海量binlog及从库串行回放,需分批+游标+显式commit。

大事务是锁等待和主从延迟的共同源头,不是“可能引发”,而是“必然触发”——只要它在主库执行,从库就大概率卡住、业务就大概率报超时。根治方式不是调参或加机器,是让事务体积可控、边界清晰、提交及时。
UPDATE/DELETE 大表时为什么总卡住?
根本原因不是语句本身慢,而是它在 ROW 格式下生成海量 binlog 事件,并在从库串行回放;同时主库持有行锁时间过长,阻塞其他事务,从库 SQL Thread 还得等它全部执行完才能推进。
- 常见错误现象:
SHOW PROCESSLIST中看到UPDATE或DELETE长时间处于Updating状态;从库Slave_SQL_Running_State卡在executing event,Exec_Master_Log_Pos几乎不动 - 别信
LIMIT 1000 OFFSET N:OFFSET 越大扫描越深,且并发执行时容易漏行或重复 - 必须用游标推进:例如
WHERE id > @last_id AND status = 'pending' ORDER BY id LIMIT 1000,每次取完立刻记录@last_id := MAX(id) - 每批必须显式
COMMIT,否则仍是单一大事务;初始化@last_id很关键,否则第一次可能全表扫描
INSERT INTO SELECT 类操作怎么拆才安全?
这类语句最容易被忽略,因为它看起来只是“一条 SQL”,但实际会把源表全量扫描一遍,锁住所有匹配行,binlog 体积爆炸,从库回放压力极大。
- 绝对避免无条件
INSERT INTO dst SELECT * FROM src;必须加确定性范围,如WHERE id BETWEEN 100001 AND 200000 - 范围要不重叠、可覆盖全量,建议按主键或自增 ID 分段,每批控制在 5k–10k 行(视单行大小和索引效率调整)
- 目标表必须有合适索引,否则每批
INSERT可能因排序或锁表变慢,反而抵消拆分收益 - 业务层更稳妥:批量导入时,每 500–1000 条封装为一个事务,而不是包成一个大事务提交
哪些“小语句”其实暗藏大事务风险?
事务大小 ≠ SQL 长度,而取决于实际修改的数据页数、锁持有时间和 binlog 事件数量。以下几类看着轻,实则危险:
UPDATE t SET status=1 WHERE create_time :没索引就全表扫,锁住数万行,binlog 写几十万条 <code>Update_rows_log_event-
ALTER TABLE t ADD COLUMN x INT DEFAULT 0:MySQL 5.7+ 虽支持 online DDL,但它仍是长事务,会阻塞 SQL Thread -
UPDATE t1 SET a=(SELECT b FROM t2 WHERE t2.id=t1.id):若t2.id无索引,每个t1行都触发一次全表扫描,从库回放时还要再扫一遍 - 高频小事务(如 autocommit=1 下每条 INSERT 都提交):虽单次快,但 IO 压力抬高,间接拖慢 relay log 应用速度
监控是否真生效,别只看 Seconds_Behind_Master
Seconds_Behind_Master 经常失真:主从时钟不同步时为负;大事务执行中它只记 BEGIN 时间戳,不反映真实进度;IO 线程还没拉完日志时它显示 0。
- 更可靠指标:
pt-heartbeat写心跳表,对比主从时间差(秒级精度) - 看
Read_Master_Log_Pos和Exec_Master_Log_Pos差值:差得大说明 IO 拉取慢;差得小但延迟高,基本确定是 SQL Thread 卡住了 - 查 GTID 差值:
SELECT GTID_SUBTRACT(@@global.gtid_executed, @@global.gtid_slave_pos),结果非空说明确实落后 - 从库开
slow_query_log并设long_query_time = 2,确认慢日志里不再出现执行超 5 秒的 DML
真正卡住同步的往往不是吞吐量,而是单点阻塞。把“改完再提交”变成“改一点、提交、再改一点”,从库才能喘得上气——这个逻辑必须落到每一行 SQL 的写法里,而不是只靠配置调优。











