发布时间:2026-09-24 12:31 更新时间:2026-09-24 12:31 阅读量:0
做站长或运维的朋友,大概都遇到过这种场面:半夜监控告警说磁盘使用率一路往上冲,进去一看是 ibdata 或 undo 表空间在疯长;与此同时从库的 Seconds_Behind_Master 从几秒爬到几千秒,业务读到的数据越来越旧。很多时候根因不是慢查询,而是某个业务代码里开了一个大事务——一次性更新几十万行、批量导入不设上限,事务迟迟不提交。本文从原理讲到实操,教你用 information_schema.innodb_trx 把长事务揪出来,再通过拆分提交把 undo 和延迟一起压下去。
InnoDB 的 MVCC 靠 undo 记录实现。事务修改一行时,旧版本会写进 undo,供其他事务读一致性快照。正常情况下,事务一提交,undo 对应的回滚段就可以被后台 purge 线程回收。但只要事务还在运行,它产生的 undo 就不能清理;即使事务提交了,如果还存在更早开始、尚未结束的读事务(长查询、长连接里没提交的快照),purge 也会被卡住,undo 只能继续堆积。
MySQL 8 默认把 undo 放在独立的 undo 表空间里(由 innodb_undo_tablespaces 控制,具体数量以实际环境为准),文件命名类似 undo_001、undo_002。它们可以自动扩展,但默认不会自动收缩——事务结束、purge 完成后,空间只是标记为可复用,文件大小不会立刻降回来。所以运维看到的"磁盘被 undo 吃掉",往往是长期积累的结果,而不是某一次写入造成的。
主从延迟的链路更直接:binlog 以事务为单位记录,一个事务在从库上要么整体执行,要么整体回滚。一个跑了十分钟的大事务,在主库上分多次写入,在从库上却只能串行重放这一个大事务,期间后面的小事务全被堵住。加上从库通常是单线程或多线程按库/按组并行,写热点集中在一张表时并行度根本用不上,延迟就这么叠起来了。
定位的第一步是找出"谁在跑、跑了多久、改了多少行"。MySQL 8 的 information_schema.innodb_trx 提供了每个活跃事务的开始时间、状态和已修改行数,配合 performance_schema 或 sys 库能直接看到 SQL 文本。
-- 按运行时长倒序,找出最老的事务
SELECT trx_id,
trx_mysql_thread_id AS thread_id,
trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS running_secs,
trx_rows_modified,
trx_state,
trx_isolation_level
FROM information_schema.innodb_trx
ORDER BY trx_started ASC
LIMIT 10;
-- 关联线程信息,拿到具体 SQL 和来源客户端
SELECT t.trx_id, t.trx_mysql_thread_id, t.trx_rows_modified,
p.user, p.host, p.db, p.command, p.time, p.info
FROM information_schema.innodb_trx t
JOIN information_schema.processlist p
ON p.id = t.trx_mysql_thread_id
ORDER BY p.time DESC;
trx_rows_modified 很大(比如几十万)且 running_secs 持续增长,基本就是大事务实锤。trx_state 若长时间停在 RUNNING,说明它在不停干活;如果停在 LOCK WAIT,那它可能还堵着别人。这里要提醒一句:不要看到长事务就直接 KILL,先确认它对应的业务和影响面,否则可能回滚更久——一个改了几十万行的事务被 kill,回滚过程本身也会产生大量 undo 并持续占用 IO。
判断 undo 到底被占了多少,可以查状态变量和文件大小:
SHOW GLOBAL STATUS LIKE 'Innodb_undo_tablespaces%';
SHOW GLOBAL STATUS LIKE 'Innodb_purge%';
-- 观察 History list length,数值持续偏高说明 purge 追不上
SHOW ENGINE INNODB STATUS\G
-- 宿主机上看 undo 文件实际占用(路径以实际配置为准)
ls -lh /var/lib/mysql/undo_*
du -sh /var/lib/mysql/undo_*
SHOW ENGINE INNODB STATUS 输出里的 History list length 是重要信号:它表示还没被 purge 的 undo 版本数量,长期处于几万甚至几十万,说明有老事务或长快照压着 purge。Innodb_purge 相关的状态项可用来观察 purge 是否在推进,具体字段名以你所用小版本的官方文档为准。
找到大事务之后,治本的做法是把大事务拆成可控批次。原则很简单:每批只改一部分行,提交一次,让 undo 有机会被回收,也让从库能跟上。下面是一个典型的循环拆分写法,适合一次性更新历史数据的场景:
-- 循环分批更新:每批 2000 行,批间短暂休眠,避免长时间持锁
SET @batch = 2000;
SET @affected = 1;
WHILE @affected > 0 DO
UPDATE orders
SET status = 'archived'
WHERE status = 'closed'
AND updated_at < '2025-01-01'
LIMIT @batch;
SET @affected = ROW_COUNT();
COMMIT;
DO SLEEP(0.2);
END WHILE;
这段逻辑在存储过程或应用层循环里都适用,关键点是三点:带 LIMIT(别一次改全表)、批间 COMMIT(释放 undo 和锁)、按主键或索引列排序筛选(避免每批都全表扫)。批大小没有万能值,2000 到 10000 之间按你的行宽和从库承受力实测调整,以实际环境压测为准。批量导入同理,用 LOAD DATA 或分批 INSERT 替代一个巨大的 INSERT ... SELECT。
拆分之后,如果 undo 文件已经涨得很大,可以评估回收。MySQL 8 支持通过 innodb_undo_log_truncate 让 undo 表空间在超过阈值(innodb_max_undo_log_size,默认 1G 左右)后被截断,前提是有多个 undo 表空间且 purge 正常推进。配置示例:
[mysqld]
innodb_undo_log_truncate = ON
innodb_max_undo_log_size = 1G
innodb_purge_rseg_truncate_frequency = 128
改完需要重启或按官方说明动态调整,具体生效方式以官方文档为准。注意截断只能回收"已经不再需要"的空间,如果长事务还在,怎么调都不会瘦。另外,监控上建议把 History list length 和 innodb_trx 里最长事务时长做成告警项,比等磁盘满了再处理要主动得多。
回到主从延迟,拆分提交之后从库通常是"小事务快速重放",延迟会明显收敛。如果延迟仍然存在,再去看 SHOW REPLICA STATUS 里的 Seconds_Behind_Master 和 Relay_Log_Pos 变化,结合从库是否开了并行复制、写入是否集中在一个库来判断下一步优化方向。
一句话总结:大事务不是"等它跑完就好"的问题,它同时消耗 undo 空间、锁资源和复制吞吐。用 innodb_trx 先定位,用分批提交去拆解,再用 undo 截断参数做空间回收,是一条可以按顺序落地的路径。建议先在测试库验证批大小和业务影响,再上生产。
| 📑 | 📅 |
|---|---|
| Nginx 443 端口 SNI 分流:WebSocket 与普通请求共用配置 | 2026-09-24 |
| Docker 容器内 Nginx 与宿主 Nginx 端口冲突排查 | 2026-09-24 |
| Nginx 与 CDN 回源 IP 不一致:XFF 取值顺序与日志字段 | 2026-09-24 |
| Linux负载高但CPU空闲:D状态进程与不可中断睡眠排查 | 2026-09-24 |
| Redis 主从切换后写入报 READONLY:三种拓扑的误配排查 | 2026-09-23 |
| Docker 容器内 systemctl 不可用:init 缺失与多进程管理的取舍 | 2026-09-24 |
| Nginx 反代后协议错乱:X-Forwarded-Proto 排查与修复 | 2026-09-24 |
| Linux 服务器 OOM Killer 日志怎么看:dmesg 与 journalctl 定位被杀进程 | 2026-09-25 |
| 内核日志里的 OOM 线索:oom_score、cgroup 限制与判断内存不足原因 | 2026-09-25 |
| Nginx与上游服务时间不同步导致签名校验失败排查 | 2026-09-25 |