MySQL 8 大事务致 undo 膨胀与主从延迟:定位与拆分提交

    发布时间:2026-09-24 12:31 更新时间:2026-09-24 12:31 阅读量:0

    做站长或运维的朋友,大概都遇到过这种场面:半夜监控告警说磁盘使用率一路往上冲,进去一看是 ibdata 或 undo 表空间在疯长;与此同时从库的 Seconds_Behind_Master 从几秒爬到几千秒,业务读到的数据越来越旧。很多时候根因不是慢查询,而是某个业务代码里开了一个大事务——一次性更新几十万行、批量导入不设上限,事务迟迟不提交。本文从原理讲到实操,教你用 information_schema.innodb_trx 把长事务揪出来,再通过拆分提交把 undo 和延迟一起压下去。

    一、大事务为什么会让 undo 膨胀、从库掉队

    InnoDB 的 MVCC 靠 undo 记录实现。事务修改一行时,旧版本会写进 undo,供其他事务读一致性快照。正常情况下,事务一提交,undo 对应的回滚段就可以被后台 purge 线程回收。但只要事务还在运行,它产生的 undo 就不能清理;即使事务提交了,如果还存在更早开始、尚未结束的读事务(长查询、长连接里没提交的快照),purge 也会被卡住,undo 只能继续堆积。

    MySQL 8 默认把 undo 放在独立的 undo 表空间里(由 innodb_undo_tablespaces 控制,具体数量以实际环境为准),文件命名类似 undo_001、undo_002。它们可以自动扩展,但默认不会自动收缩——事务结束、purge 完成后,空间只是标记为可复用,文件大小不会立刻降回来。所以运维看到的"磁盘被 undo 吃掉",往往是长期积累的结果,而不是某一次写入造成的。

    主从延迟的链路更直接:binlog 以事务为单位记录,一个事务在从库上要么整体执行,要么整体回滚。一个跑了十分钟的大事务,在主库上分多次写入,在从库上却只能串行重放这一个大事务,期间后面的小事务全被堵住。加上从库通常是单线程或多线程按库/按组并行,写热点集中在一张表时并行度根本用不上,延迟就这么叠起来了。

    二、用 innodb_trx 定位长事务,并判断 undo 压力

    定位的第一步是找出"谁在跑、跑了多久、改了多少行"。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