什么是 purge 线程?undo 历史记录什么时候被清理?
What is the purge thread? When is the undo history purged?
考察要点
这道题面试官想看你是否深刻理解 InnoDB 的 MVCC (多版本并发控制) 机制、undo log 的作用以及后台垃圾回收(Garbage Collection) 的原理。核心是考察你是否知道一个旧的数据版本何时才“安全”地可以被物理删除,这直接关联到数据库的并发性能和空间管理。
核心概念
要理解 purge 线程,必须先理解 undo log 在 MVCC 中的核心角色。
1. Undo Log 与 MVCC
当一个事务对记录进行修改(UPDATE 或 DELETE)时,InnoDB 不会直接覆盖原始数据,而是:
- 将原始数据(旧版本)拷贝一份到 undo log 中。
- 修改聚簇索引中的记录,并在记录的隐藏列中记录下指向该 undo log 的指针 (
DB_ROLL_PTR)。 - 记录的另一个隐藏列
DB_TRX_ID会记录下修改它的事务 ID。
这样,一行记录就可能存在多个版本,通过 DB_ROLL_PTR 串联成一个版本链 (Version Chain)。
数据示例:
假设有一张表 users,初始数据如下:
1CREATE TABLE users (2 id INT PRIMARY KEY,3 name VARCHAR(20),4 balance INT5) ENGINE=InnoDB;67INSERT INTO users VALUES (1, 'Alice', 1000);
| id | name | balance | DB_TRX_ID | DB_ROLL_PTR |
|---|---|---|---|---|
| 1 | Alice | 1000 | 10 | (null) |
现在,一个事务 TRX_ID = 25 执行 UPDATE users SET balance = 900 WHERE id = 1;
- 写
Undo Log:将(id=1, name='Alice', balance=1000, DB_TRX_ID=10)这条旧记录写入 undo log。 - 更新记录:将表中的记录更新为
(id=1, name='Alice', balance=900),并更新隐藏列DB_TRX_ID=25,DB_ROLL_PTR指向刚才写入的 undo log 记录。
版本链如下所示:
1graph TD2 A["当前记录<br/>(balance=900, TRX_ID=25)"] -- DB_ROLL_PTR --> B["Undo Log 中的旧版本<br/>(balance=1000, TRX_ID=10)"];
这个版本链有两个主要作用:
- 事务回滚:如果事务 25 需要回滚,可以直接利用 undo log 中的旧版本恢复数据。
- 一致性读 (Consistent Read):如果另一个事务在事务 25 提交前查询
id=1的记录,它可以沿着版本链找到早于它启动时已提交的版本,从而实现非阻塞读。
2. Read View (一致性视图)
当一个事务(隔离级别为 RR 或 RC)开始时,它会创建一个 Read View。Read View 决定了该事务能“看到”哪些数据版本。它主要包含三个关键信息:
m_ids: 创建 Read View 时,系统中所有活跃(未提交)的事务 ID 列表。min_trx_id:m_ids列表中的最小事务 ID。max_trx_id: 创建 Read View 时,系统下一个将要分配的事务 ID。
可见性判断规则:当访问一条记录时,会检查其 DB_TRX_ID:
- 如果
DB_TRX_ID < min_trx_id,说明这个版本在当前事务启动前就已提交,可见。 - 如果
DB_TRX_ID >= max_trx_id,说明这个版本是在当前事务启动后才开启的事务创建的,不可见。 - 如果
min_trx_id <= DB_TRX_ID < max_trx_id,则需要检查DB_TRX_ID是否在m_ids列表中:- 如果在,说明创建该版本的事务在当前事务启动时仍活跃,不可见。
- 如果不在,说明创建该版本的事务在当前事务启动前已提交,可见。
如果当前版本不可见,则顺着 DB_ROLL_PTR 去版本链中找上一个版本,重复此过程,直到找到一个可见的版本或链表末尾。
3. Purge 线程与清理时机
Purge 线程是 InnoDB 的后台垃圾回收线程,专门负责清理不再需要的 undo log 记录。
清理时机/条件: 一条 undo log 记录可以被清理,必须同时满足两个条件:
- 创建该 undo log 的事务已经提交 (Commit)。如果事务还未提交,可能需要回滚,undo log 绝对不能删除。
- 该 undo log 对应的旧版本,不再被任何活跃的事务需要。即,系统中现存的最老的 Read View 也看不到这个版本了。
InnoDB 会维护一个全局的 purge_view,它代表了系统中所有活跃 Read View 的“并集”。Purge 线程会根据这个 purge_view 来判断。简单来说,如果一个 undo log 记录的 DB_TRX_ID 小于当前所有活跃事务中最小的事务 ID (min_trx_id),那么这条 undo log 记录就是安全的,可以被清理。
清理过程:
- Purge 线程从 history list (一个记录了已提交事务的 undo log 链表) 中选取待清理的 undo log。
- 根据上述清理条件,判断 undo log 是否可以被物理删除。
- 如果可以,就将其从版本链中移除,并标记 undo log 页中的空间为可重用。这个过程最终会释放 undo tablespace 中的空间。
4. 版本差异
-
MySQL 5.7 及之前:
- undo log 默认存储在系统表空间 (
ibdata1) 中。这导致ibdata1文件会只增不减,即使删除了大量数据,空间也无法回收,造成巨大的空间浪费。 - 通常只有一个 purge 线程 (
innodb_purge_threads=1),在高并发更新、删除场景下可能成为瓶颈。
- undo log 默认存储在系统表空间 (
-
MySQL 8.0 及之后:
- undo log 默认存储在独立的 undo 表空间中(
undo_001,undo_002...)。通过innodb_undo_tablespaces参数控制。 - 独立的 undo 表空间支持在线收缩 (Truncate),解决了
ibdata1无法收缩的痛点。 innodb_purge_threads默认值为 4,可以配置多个 purge 线程,显著提升了在高并发 DML 负载下的垃圾回收效率。
- undo log 默认存储在独立的 undo 表空间中(
常见低分回答
- “事务一提交,undo log 马上就被删除了。”
- 错误:这是最常见的误解。忽略了
MVCC的存在。即使事务已提交,其创建的 undo log 可能仍然需要服务于其他更早启动的、但尚未结束的读事务。
- 错误:这是最常见的误解。忽略了
- “Undo log 就是用来做回滚的,回滚完就没用了。”
- 片面:只说对了一半。Undo log 的两大作用是事务回滚和实现
MVCC。在已提交的场景下,它主要是为了一致性读服务。
- 片面:只说对了一半。Undo log 的两大作用是事务回滚和实现
- “Purge 线程就是定期清理日志文件的。”
- 模糊不清:没有说明白清理的是什么日志(undo log),也没有讲清楚清理的核心条件(不再被任何活跃事务的 Read View 需要)。
- “只要有长时间不提交的事务,数据库就会变慢。”
- 正确但不深入:没有解释清楚“为什么”。根源在于长事务导致 Read View 无法前进,
min_trx_id保持一个很小的值,使得大量的 undo log 无法被 purge,导致 history list 过长。这不仅会撑爆 undo 空间,还会让后续的所有查询在遍历版本链时做更多的工作,从而导致性能急剧下降。
- 正确但不深入:没有解释清楚“为什么”。根源在于长事务导致 Read View 无法前进,
工程实践与优化
- 避免长事务:这是防止 undo 空间膨胀和性能下降的根本。任何一个长时间运行的查询(即使是
SELECT)或者一个开启后长时间不COMMIT/ROLLBACK的 DML 事务,都会成为 purge 的“拦路虎”。- 监控:通过
information_schema.innodb_trx表可以查到长时间运行的事务。sql1SELECT trx_id, trx_state, trx_started, trx_query2FROM information_schema.innodb_trx3ORDER BY trx_started ASC;
- 监控:通过
- 监控 History List Length:这是衡量 undo log 堆积情况的核心指标。
在sql1SHOW ENGINE INNODB STATUS;
TRANSACTIONS部分查看History list length的值。如果这个值持续增长且居高不下,说明 purge 受阻,需要立即排查长事务。 - 合理配置 Purge 相关参数:
innodb_purge_threads(MySQL 8.0+): 在多核 CPU 和高 DML 负载下,适当增加此值(如 4-8)可以提升清理效率。innodb_max_purge_lag: 当 history list 长度超过此阈值时,InnoDB会开始延迟后续的 DML 操作(通过innodb_max_purge_lag_delay控制延迟时间),给 purge 线程留出追赶的时间,这是一种自我保护机制。
- 使用独立 Undo 表空间 (MySQL 8.0+):
- 务必开启独立 undo 表空间,便于空间管理和监控。
- 当 undo 表空间过度膨胀后,可以通过在线方式禁用、重建、再启用某个 undo 表空间来回收磁盘空间,操作虽然复杂但可行。
常见追问与延伸题目
-
追问:一个长时间运行的
SELECT查询,为什么会影响整个系统的性能?- 回答要点:
- 阻塞 Purge:这个
SELECT事务持有一个非常早的 Read View,其min_trx_id很小。这导致在它运行期间发生的所有数据变更产生的 undo log 都无法被 purge 线程清理。 Undo Log堆积:这会造成History list length持续增长,undo 表空间急剧膨胀,最坏情况会耗尽磁盘空间。- 查询性能下降:对于所有在这个长查询之后启动的新查询,当它们访问被频繁更新的行时,需要遍历很长的版本链才能找到对自己可见的版本,导致查询耗时显著增加。
- 系统整体吞吐量下降:由于查询变慢,系统资源(CPU, IO)被更多地消耗在版本链的遍历上,整体的 DML 和 QPS 都会受到影响。
- 阻塞 Purge:这个
- 回答要点:
-
追问:如果
History list length已经非常高(例如几百万),有什么紧急处理手段?- 回答要点:
- 定位并终止长事务:这是最直接有效的办法。通过查询
information_schema.innodb_trx找到启动时间最早的事务,分析其trx_query。如果确认是可杀掉的(例如一个不重要的离线分析查询),立即使用KILL <thread_id>将其终止。 - 调整 Purge Lag 参数:如果不能立即杀死事务,可以临时调大
innodb_max_purge_lag,避免 DML 操作被过度延迟,但这只是权宜之计,治标不治本。 - 业务侧排查:从应用层面检查是否有未关闭的数据库连接、忘记提交的事务、或者设计不合理的定时/批量任务。
- 资源扩容:如果磁盘空间即将耗尽,紧急扩容磁盘是最后的救命稻草。
- 定位并终止长事务:这是最直接有效的办法。通过查询
- 回答要点:
-
追问:PostgreSQL 是如何处理这个问题的?和 MySQL 的 Purge 有什么异同?
- 回答要点:
- 相似机制:PostgreSQL 同样基于
MVCC,当数据更新时,它不会在原地修改,而是插入一个新版本的数据行(称为tuple),并将旧tuple标记为“已过期”。这个旧tuple类似于 MySQL 的 undo log 记录,在过期前对其他事务可见。 - 不同的实现:VACUUM:PostgreSQL 的垃圾回收机制叫做
VACUUM。它和 Purge 的目标一致:回收不再被任何事务可见的旧版本数据所占用的空间。 - 主要区别:
- 回收对象:MySQL Purge 清理的是 undo log。而 PostgreSQL
VACUUM清理的是表文件中直接标记为“死”的tuple(行版本)。 - 空间回收:
VACUUM只是将空间标记为可重用,但通常不会将空间返还给操作系统。要缩小表文件,需要VACUUM FULL,但这会锁住全表。MySQL 8.0 的 undo 表空间收缩是线上的,更灵活。 - 触发方式:两者都有自动触发机制。PostgreSQL 的
autovacuum守护进程会根据表上死tuple的数量和比例自动执行VACUUM。MySQL 的 Purge 线程是持续在后台运行的。 - 膨胀问题:PostgreSQL 同样面临“表膨胀”问题,如果
autovacuum不及时或被长事务阻塞,表文件会变得非常臃肿。这与 MySQL 的 undo 表空间膨胀问题本质上是同源的。
- 回收对象:MySQL Purge 清理的是 undo log。而 PostgreSQL
- 相似机制:PostgreSQL 同样基于
- 回答要点: