回滚卡住主因是undo page随机IO扫盘,而非撤销逻辑慢;表现为History list length超5000、await>20ms、%util=100%,根本原因是长事务阻塞purge导致undo空间耗尽且无法释放。

回滚卡住其实是undo page随机IO在扫盘
大事务回滚本身不“慢”,但会触发大量分散的 undo page 随机读——每个要回滚的行版本,都可能落在磁盘不同位置的 undo segment 里。InnoDB 没法批量顺序读,只能跳着找,磁盘寻道时间暴涨。一旦 History list length 超过 5000,iostat -x 1 就会看到 await > 20ms 且 %util = 100%,这不是 CPU 或锁的问题,是物理盘被拖死了。
- undo page 分散存储 + 回滚必须逐条反向应用 → 强制高随机 IO
- buffer pool 不缓存 undo page(默认行为),每次都要从磁盘读
- 如果 innodb_buffer_pool_size 太小,还会挤出数据页缓存,加剧二次 IO
purge 线程被长事务堵死,undo 空间无法释放
只要有一个活跃长事务没提交,它产生的所有 undo log 就不能被 purge 清理。History list length 就会持续上涨,新事务写入时又不断追加 undo,形成正反馈。这时你看到 INSERT 报 The table 'xxx' is full,不是表满了,是 undo 表空间爆了——删业务数据、清 binlog 都没用,因为 undo 不随数据删而释放。
- 查长事务:
SELECT trx_id, trx_started, trx_rows_modified FROM information_schema.INNODB_TRX WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 600 - 确认 purge 滞后:
SHOW ENGINE INNODB STATUS\G里搜HISTORY LIST LENGTH,> 5000 就危险 - 检查是否启用独立 undo 表空间:
SELECT VARIABLE_VALUE FROM performance_schema.global_variables WHERE VARIABLE_NAME = 'innodb_undo_tablespaces',值必须 > 0 才能后续缩容
KILL 后反而更慢?因为回滚根本停不下来
KILL 只断开客户端连接,事务回滚仍在后台线程中继续执行。而且 InnoDB 会在当前正在处理的 undo page 完成后再退出——如果你 KILL 一个已运行 12 分钟、还剩 85% undo 没处理的事务,它很可能再跑 15 分钟才真正结束。这不是 bug,是 ACID 保证机制:已写入的 undo 必须清理干净,否则 MVCC 读会看到脏历史。
- 别信“KILL 就能立刻释放资源”,它只是把前台阻塞转为后台隐形消耗
- 用
pt-kill --match-state="Rolling back" --busy-time=300可以辅助识别,但不能替代诊断 - 真要干预,优先调
innodb_purge_rseg_truncate_frequency = 1加速 purge,而不是杀线程
为什么 innodb_force_recovery=3 是最后手段,且必须重启?
设 innodb_force_recovery = 3 的本质是让 InnoDB 启动时不执行事务回滚(SRV_FORCE_NO_TRX_UNDO),跳过 undo 解析阶段。但它只在实例启动时生效,运行中无法动态开启;而且启用后所有 DML(INSERT/UPDATE/DELETE)都被禁用,只能做 SELECT、DROP、CREATE。所以必须改配置 → 重启 → 删问题表 → 改回配置 → 再重启。过程中若误设为 4 或更高,可能跳过 redo 前滚,导致未刷盘变更丢失。
- 启用前务必确认:没有其他服务正在写入该实例,且有完整备份
- 重启后立即
DROP TABLE,别犹豫——因为此时无法做任何 DML,拖得越久风险越大 - 恢复后必须检查外键约束:
SET FOREIGN_KEY_CHECKS=0再导入,否则mysql导入会卡在外键校验上
真实线上最常被忽略的一点:很多团队查到长事务后直接 KILL,却忘了查 innodb_force_recovery 是否被遗留开启过。一旦这个参数残留在配置文件里,下次崩溃恢复就会跳过 undo 扫描,回滚静默失败,报错却是 Unknown error,排查方向完全跑偏。











