根本原因不是“回滚慢”,而是UPDATE每行实时写undo record,持续压垮I/O和buffer pool;事务未提交时Undo空间已撑爆,History list length超5000、%util=100%即为铁证。

根本原因不是“回滚慢”,而是UPDATE过程中每行都实时写入一条undo record,持续压垮I/O和buffer pool——事务还没提交,Undo空间就已经被撑爆了。
UPDATE每一行就写一条undo record,不等事务结束
MySQL在执行UPDATE时,只要扫描到一行并决定修改,就会立刻生成该行的前镜像(old version)并写入undo log。哪怕你只改一个字段、值也没变(如SET status = 1),InnoDB仍会记完整undo记录。
- 更新50万行 → 至少生成50万条undo记录,全部刷盘、占buffer pool、拉长版本链
-
SHOW ENGINE INNODB STATUS\G里History list length持续超5000,说明purge线程已被卡住 -
iostat -x 1中%util == 100%、await > 20ms,是I/O早已饱和的铁证
WHERE条件没走索引,undo爆炸速度翻倍
全表扫描式UPDATE(如UPDATE t SET a = 1)会让InnoDB遍历聚簇索引每一行,每行都触发undo写入;而带索引WHERE的UPDATE只触达目标行,undo量可控。
- 错误写法:
WHERE DATE(created_at) = '2024-01-01'→type: ALL,全表扫 - 正确写法:
created_at >= '2024-01-01' AND created_at → 走索引,type: range - 联合索引顺序错位(如索引是
(updated_at, status)但WHERE用status = 0 AND updated_at > ...)→ 索引失效,undo照常爆炸
KILL后仍卡住,是因为undo早已写满,不是回滚慢
很多人以为KILL掉大事务就能立刻释放资源,实际并非如此:KILL只是中断当前语句,但事务已生成的undo记录仍在磁盘上,且必须由purge线程逐条清理。如果源头长事务没杀干净,purge永远跟不上。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- 先查真正元凶:
SELECT trx_id, trx_started, trx_state, trx_rows_modified FROM information_schema.INNODB_TRX ORDER BY trx_started LIMIT 5 - 重点关注
trx_state = 'RUNNING'且trx_query IS NULL的空闲事务——它们钉住所有早于它的undo log -
KILL前务必确认状态:trx_state = 'ROLLING BACK'时别动,否则回滚更慢、更占IO
分批UPDATE不是万能解药,批大小必须按实测定
把百万行拆成1000行/批只是起点,真正决定成败的是每批是否真正释放了undo压力。批太小,事务开销占比高;批太大,仍可能OOM或I/O打满。
- 必须用主键范围切片:
UPDATE t SET status = 1 WHERE id BETWEEN ? AND ? AND status = 0,禁用LIMIT无序分页 - 每批后显式
COMMIT,不能等脚本结束才提交 - 上线前必须在影子库压测:监控
Innodb_buffer_pool_wait_free、ps aux --sort=-%mem中mysqld的RSS增长曲线 - 隐性内存杀手要关:
performance_schema中memory/sql/sp_head采集、过大的sort_buffer_size、触发器逻辑堆在main_mem_root里不释放
最容易被忽略的是:Undo空间爆满从来不是孤立现象,它总是和Sleep连接堆积、MDL锁排队、CPU飙升同步发生——看到其中一个,就要立刻去查INNODB_TRX,而不是调innodb_max_purge_lag这种参数。










