长事务是undo log堆积的唯一根源,所有数据库层参数调整都只是缓释手段;真正要做的,是在应用连接发起sql前,把事务生命周期压到最小。

长事务是 Undo Log 堆积的唯一根源,所有数据库层参数调整都只是缓释手段;真正要做的,是在应用连接发起 SQL 前,把事务生命周期压到最小。
为什么事务一长,Undo 就停不下来
只要一个事务没提交,InnoDB 就必须保留它开始时刻之前的所有行旧版本——不是“这条记录的 undo”,而是“所有被它可见性覆盖的 undo 记录”。哪怕这个事务只执行了一条 SELECT * FROM user WHERE id = 123 后就空闲着,history_list_length 也会持续上涨,purge 线程寸步难行。
常见错误现象包括:
-
SHOW ENGINE INNODB STATUS\G中History list length超过 5000 且缓慢爬升 -
information_schema.INNODB_TRX里出现trx_state = 'RUNNING'但trx_query IS NULL的记录 - 磁盘空间告警,而
ibdata1或undo_001文件体积持续增长
关联查询场景下最容易藏长事务的三个地方
多表 JOIN、子查询、ORM 预加载(如 Django select_related / MyBatis <association></association>)本身不危险,危险的是它们被包进一个本不该存在的事务里。
- 在事务中调用 RPC 接口获取关联数据(比如查订单后远程调用户服务),然后才更新订单状态——RPC 耗时直接变成事务持有时间
- 用 ORM 一次性加载 1000 条主记录 + 每条关联 5 张表,生成 5000+ 行结果,再在事务内做业务判断——结果集构建过程全在事务里
- 为“保证一致性”把读操作也塞进写事务:例如先
SELECT FOR UPDATE查库存,再调外部风控,最后UPDATE;其实风控不改库,完全可移到事务外
怎么让关联逻辑不拖慢事务
核心原则:事务只包裹真正需要原子性的数据库写操作,其他一律剥离。
- 把关联数据的获取拆到事务前:用缓存、异步消息或只读连接预取,避免在写事务中做
JOIN或子查询 - 对必须关联的场景,改用显式分步:
SELECT id, user_id FROM order WHERE status = 'pending' LIMIT 100→ 提交 → 再用这 100 个user_id批量查用户信息 → 提交 → 最后批量更新订单 - 检查 ORM 配置是否隐式开启事务:Spring Boot 默认
@Transactional作用在 Controller 层,会导致整个 HTTP 请求生命周期都被锁住;应下沉到 Service 方法粒度,并加propagation = Propagation.REQUIRES_NEW隔离非关键读 - 对只读关联,显式声明
START TRANSACTION READ ONLY,InnoDB 不分配 undo segment,彻底规避写 undo 开销
紧急情况下如何快速定位并切断
别等磁盘爆满。每天定时跑一次以下语句,重点关注 duration_sec 和 trx_query:
SELECT trx_id, trx_started, TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) AS duration_sec, trx_state, trx_mysql_thread_id, trx_query FROM information_schema.INNODB_TRX WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 300;
如果发现 duration_sec > 600 且 trx_query IS NULL,立刻查 information_schema.PROCESSLIST 对应线程的 HOST 和 USER,联系对应服务负责人——这类连接大概率是 Python/Java 进程异常退出后未关闭连接,而不是业务逻辑必需。
真正难处理的,永远不是那个运行了 2 小时的事务,而是你还没意识到它正在把整个实例的 purge 线程钉死在原地。










