十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MySQL Undo Log 深度解析:事务回滚与 MVCC 的底层支柱

MySQL Undo Log 深度解析:事务回滚与 MVCC 的底层支柱 一、引言理解事务与并发之前先读懂 Undo Log在 MySQL 的日常开发中我们几乎每天都在与事务打交道开启事务、执行 DML、提交或回滚。很多开发者能够熟练使用事务的四大特性 ACID也知道 InnoDB 之所以能支持事务与多版本并发控制MVCC离不开 Redo Log、Binlog 和 Undo Log 三大日志体系的协同工作。但真正深入到底层时Undo Log 往往是最容易被低估、也最容易被误解的一环。一个常见的误区是很多人认为 Undo Log 就是用来做事务回滚的仅仅在事务失败或者用户手动执行 ROLLBACK 时才发挥作用。事实上Undo Log 的职责远远不止于此。它既是「事务回滚」的物理基础也是「MVCC 多版本并发控制」能够实现读不阻塞写、写不阻塞读的关键支柱。如果把 InnoDB 的事务机制比作一座建筑那么 Undo Log 就是埋在地基里的钢筋——平时看不见却决定了整座建筑的稳定性。本文将从「是什么、为什么、怎么用、怎么优化」四个维度对 MySQL InnoDB 的 Undo Log 进行系统性的深度解析。内容涵盖 Undo Log 的物理存储结构、逻辑记录格式、事务回滚机制、MVCC 实现原理、Purge 清理机制、与 Redo Log 的协同关系以及生产环境中的参数调优与故障排查方法。希望通过这篇文章能够帮助读者建立起对 Undo Log 的完整认知真正理解事务回滚与 MVCC 的底层支柱是如何运行的。在开始之前需要先明确一个范围本文讨论的是 InnoDB 存储引擎中的 Undo Log。MySQL 服务层的 Binlog 是记录逻辑变更的归档日志MGR 等组件也有自己的日志体系它们与 InnoDB 内部的 Undo Log 在职责和实现上是完全不同的。本文所有分析均以 MySQL 8.0 版本为主要参照同时会指出与 5.7 及更早版本之间的关键差异。二、Undo Log 是什么先建立概念边界Undo Log直译过来是「回滚日志」。它是 InnoDB 存储引擎为了支持事务的回滚操作而记录的一种逻辑日志。所谓逻辑日志是指它记录的不是磁盘上某个页面的物理字节变化而是记录了一种「逻辑上的变更描述」——例如某个事务向某张表的某一行插入了一条数据或者把某一行的某个字段从旧值修改成了新值。当需要回滚时InnoDB 会按照 Undo Log 中记录的描述执行反向操作从而把数据恢复到修改之前的状态。2.1 逻辑日志与物理日志的区别为了准确理解 Undo Log 的本质必须把「逻辑日志」和「物理日志」区分清楚。InnoDB 体系中Redo Log 是一种物理日志它记录的是对数据页的物理修改例如「在表空间 X 的第 N 个页的偏移量 M 处写入了 4 个字节的数据 0x00000001」。这种日志与存储结构强绑定必须按照物理位置原样重放redo才能恢复数据页到崩溃前的状态。与 Redo Log 不同Undo Log 记录的是逻辑操作例如「表 T 的这一行记录被插入」「这一行记录的 c1 字段从 10 变成了 20」。这些描述不关心具体的页号和偏移量反而更像是 SQL 层面反向操作的半成品模板。正因为是逻辑的Undo Log 可以被用于回滚操作——只要把记录里描述的操作反过来执行即可。也正因为是逻辑的同一个 Undo Log 记录可以被多个并发的读事务反复读取用于构建数据的历史版本这就是 MVCC 的基础。2.2 Undo Log 不是「备份」还有一个必须澄清的概念Undo Log 保存的是「这次修改之前的样子」但不要把它理解成对整行数据或者整张表的快照备份。对于一次 UPDATE 操作如果只修改了一个索引中的若干列Undo Log 通常只记录足够恢复这些列的信息而不是整行所有列的完整副本。对于一次 INSERT 操作Undo Log 则记录所插入行的主键以便回滚时能够精确地找到并删除这条数据。这种「按需记录」的设计是为了在保证可回滚能力的前提下尽量减少 Undo Log 的磁盘占用和写入开销。明确了这些边界之后我们可以给 Undo Log 下一个更完整的定义Undo Log 是 InnoDB 存储引擎为支持事务回滚与 MVCC 而维护的逻辑日志它以「撤销记录」的形式保存数据修改前的信息通过反向操作将数据恢复到修改前状态并通过版本链为一致性读提供历史数据版本。三、Undo Log 的核心作用两大职责缺一不可Undo Log 在整个 InnoDB 事务体系中承担着两个核心职责它们分别对应事务的原子性与隔离性。3.1 职责一事务回滚保证原子性Atomicity事务回滚是 Undo Log 最直观的作用。所谓原子性是指一个事务中的所有操作要么全部成功要么全部失败。当用户显式执行 ROLLBACK或者事务因为异常、死锁、超时等原因被强制终止时InnoDB 必须能够撤销这个事务已经执行过的所有变更把数据库恢复到事务开始之前的状态。这个「撤销」的过程本质上就是把事务执行期间产生的一系列 Undo Log 记录按照相反的顺序依次重放。例如一个事务依次执行了三条语句插入一行、更新一行、删除一行那么回滚时就按照「恢复删除的行、还原更新的行、删除插入的行」的顺序逐条执行撤销。这种后进先出的处理方式与程序调用栈的行为高度一致能够保证嵌套操作和先后依赖关系都不会被破坏。3.2 职责二MVCC 一致性读保证隔离性Isolation如果说事务回滚体现的是 Undo Log 的「回退」能力那么 MVCC 体现的就是它的「留存」能力。InnoDB 默认的隔离级别是可重复读REPEATABLE READ同时又以「快照读不加锁」的方式支持高并发。在这种机制下一个只读事务在开启时生成一个 ReadView读视图此后它读取到的数据必须始终是「事务快照」对应的版本而不能被其他事务后续提交的修改所干扰。假设事务 A 开启后读取了某一行数据随即事务 B 修改了这一行并提交。按照 MVCC 的要求事务 A 再次读取同一行时仍然应该看到旧值。这个「旧值」保存在哪里答案就是 Undo Log。每当事务修改数据时InnoDB 都会把修改前的值写入 Undo Log并把当前记录中的回滚指针Roll Pointer指向这条 Undo 记录。多个事务的连续修改会形成一条版本链链上的每一个节点都对应一次历史修改。一致性读沿着这条版本链回溯结合 ReadView 中的活跃事务列表判断哪个版本对当前事务可见从而呈现出稳定一致的快照数据。可以这样总结Undo Log 既负责「删除不该存在的修改」也负责「找回已经消失的历史」。前者支撑事务回滚后者支撑 MVCC。理解这两条主线是掌握 Undo Log 全部知识的前提。四、Undo Log 的物理存储从表空间到磁盘文件在理解了概念之后我们把视角下沉到物理层看看 Undo Log 到底存放在哪里、以什么形式组织。4.1 历史演进从系统表空间到独立 Undo 表空间在 MySQL 5.6 及更早的版本中Undo Log 默认存放在共享的系统表空间ibdata 文件中。这种做法带来两个明显的问题第一系统表空间的文件无法在线收缩一旦 Undo Log 膨胀即使大量数据已不再需要磁盘空间也难以释放第二Undo Log 与数据字典、Change Buffer 等其他对象混在一起既增加了管理复杂度也容易产生 I/O 争用。从 MySQL 5.7 开始InnoDB 支持配置独立的 Undo 表空间将 Undo Log 从系统表空间中剥离出来。到了 MySQL 8.0独立 Undo 表空间成为默认且强制推荐的方式。默认安装会创建两个 Undo 表空间文件undo_001 和 undo_002存放在数据目录下。这种设计使得 Undo Log 的膨胀与收缩不再受制于系统表空间也为后面的在线截断truncate操作提供了可能。4.2 Undo 表空间与回滚段Rollback Segment每个 Undo 表空间内部被划分为若干个回滚段Rollback Segment。回滚段是管理 Undo 页的容器每个回滚段内部维护一个或多个undo segment而 undo segment 又由一组连续的 undo 页组成。回滚段的引入本质上是为了在并发事务之间分摊 Undo Log 的分配与管理减少锁竞争。在 MySQL 8.0 中每个 Undo 表空间默认包含 128 个回滚段。这些回滚段被进一步划分给不同用途其中约 32 个为驻留在内存中的临时回滚段用于临时表其余为普通回滚段用于持久化表。每个回滚段又维护着 1024 个 undo slot用于分配给不同的事务。事务开启后会从某个回滚段中申请一个或多个 undo slot并在该 slot 中记录自己的 Undo 日志直到事务结束。4.3 相关参数与配置下面通过一个表格集中说明与 Undo 表空间相关的关键参数。这些参数在 MySQL 8.0 中具有代表性理解它们有助于在生产环境中做出正确决策。参数名含义默认值或说明innodb_undo_tablespacesUndo 表空间数量MySQL 8.0 中弃用由文件定义决定innodb_max_undo_log_size单个 Undo 表空间的最大大小默认 1073741824 字节即 1GBinnodb_undo_log_truncate是否开启 Undo 表空间自动截断默认 ONinnodb_undo_directoryUndo 表空间的存放目录通常指向数据目录innodb_rollback_segments回滚段数量默认 128innodb_purge_threads后台 Purge 线程数量默认 4最大 32innodb_purge_batch_size每批 Purge 处理的 Undo 页数量默认 300需要特别说明的是MySQL 8.0 中通过 SQL 语句可以动态创建和删除 Undo 表空间使 DBA 能够在实例不中断服务的前提下扩展 Undo 容量。例如可以通过CREATE UNDO TABLESPACE语句新增一个 Undo 表空间也可以通过ALTER UNDO TABLESPACE ... SET INACTIVE将其标记为不活跃并在满足条件后执行在线 shrink。4.4 文件命名与识别默认安装的 MySQL 8.0 会在数据目录生成类似undo_001、undo_002这样的文件。用户自定义的 Undo 表空间文件后缀通常为.ibu例如undo_003.ibu。这些文件在服务器启动时会被自动识别并加载前提是它们已经被记录在数据字典中。理解文件命名规则有助于在磁盘规划时预先分配足够的空间避免后续遇到容量瓶颈。五、Undo Log 的分层逻辑结构物理文件只是最外层的容器。要真正理解 Undo Log 的工作方式还需要深入它的内部逻辑结构理解版本链和记录格式是如何组织的。5.1 行记录中的关键字段在 InnoDB 的聚簇索引主键索引记录中除了用户定义的数据列外还包含若干隐藏的系统列。其中与 Undo Log 直接相关的是两个字段DB_TRX_ID事务 ID记录最后一次修改该行数据的事务 ID。它标识了「当前这个行版本是由哪个事务产生的」。DB_ROLL_PTR回滚指针指向该行上一个版本的 Undo 记录构成版本链的指针。当一行数据被事务修改时InnoDB 会在 Undo Log 中写入一条撤销记录内容包含修改前的数据或不完整信息。随后把这行新数据的回滚指针指向这条 Undo 记录并更新事务 ID 为当前事务 ID。通过不断重复这个过程一条数据的多个历史版本就被串联成了一条链表。链表的最新节点是当前数据页中的行记录更早的节点则分散存放在 Undo Log 中。5.2 版本链的形成过程举一个具体的例子来演示版本链是如何一步步构建的。假设存在一张表t(id INT PRIMARY KEY, name VARCHAR(50))初始时有一条数据(1, Alice)由事务 ID 为 100 的事务插入。初始状态行记录为(1, Alice)DB_TRX_ID 100DB_ROLL_PTR 指向插入时的 Undo 记录。事务 101 执行UPDATE t SET nameBob WHERE id1InnoDB 先写入一条 Update Undo 记录其中保存修改前的值nameAlice以及旧版本的回滚指针。然后更新行记录为(1, Bob)DB_TRX_ID 101DB_ROLL_PTR 指向刚才写入的 Undo 记录。事务 102 执行UPDATE t SET nameCindy WHERE id1同理写入新的 Undo 记录保存nameBob行记录更新为(1, Cindy)DB_TRX_ID 102DB_ROLL_PTR 指向最新 Undo 记录。经过这两次修改后版本链从新到旧依次是Cindy当前行→ BobUndo 1→ AliceUndo 2再指向插入 Undo 记录。一个需要读取旧快照的事务会沿着这条链逐步回溯直到找到对它的 ReadView 可见的版本。这里有一个容易被忽略的细节Update Undo 记录中保存的旧值字段可能并不是完整的列集合。对于非更新索引键的更新Undo 记录通常只包含被修改列以及定位所需的主键列。而对于更新了二级索引的场景InnoDB 还会额外记录被修改的二级索引信息以便回滚时同步恢复二级索引。这种按需记录的设计虽然精巧但也要求我们在分析 Undo 数据时不能简单地把它当成整行快照来理解。5.3 INSERT Undo 与 UPDATE Undo根据操作类型Undo 记录可以大致分为两类INSERT Undo和UPDATE Undo。这种分类不是可有可无的命名差异而是直接影响着后续 Purge 时机的关键因素。INSERT Undo对应 INSERT 操作。因为新插入的行对其他事务原本就不可见所以一旦事务提交这条 Insert Undo 记录就立即失去了作用不存在需要看到「插入之前」版本的其他事务。因此 Insert Undo 在事务提交后即可被安全清理。UPDATE Undo对应 UPDATE 和 DELETE 操作。由于其他事务的旧 ReadView 可能仍然需要访问被修改前的数据这类 Undo 记录在事务提交后必须保留一段时间直到确认没有任何活动事务还需要旧版本时才能被 Purge 线程清理。正是这种「Insert Undo 提交即无用、Update Undo 需要延迟清理」的差异决定了 Purge 机制的核心逻辑。理解了这一点也就理解了为什么有些批量插入场景下 Undo 空间回收很快而大量更新删除场景下 Undo 空间会长时间保持高位。六、事务回滚的内部机制有了前面的存储与结构铺垫现在可以深入分析事务回滚的完整流程。回滚看似简单但内部涉及 Undo 记录的解析、反向操作的执行、锁与事务状态的维护等一系列细节。6.1 回滚的触发场景事务回滚并非只发生在用户显式输入 ROLLBACK 的时候。实际生产环境中回滚的触发场景要丰富得多用户显式执行ROLLBACK语句。连接断开时未提交事务被服务器自动回滚。事务执行超过innodb_lock_wait_timeout设定的锁等待时间。发生死锁InnoDB 选择牺牲其中一个事务作为死锁受害者。复制、崩溃恢复等场景中部分未完成事务的清理。DDL 隐式提交之前若先前事务存在错误导致无法继续。无论由哪种场景触发回滚的底层逻辑都是统一的定位到该事务对应的 Undo 日志按相反顺序重放撤销操作。6.2 回滚的执行流程当一个事务需要回滚时InnoDB 大致按照以下步骤执行第一步定位 Undo 记录。事务在其启动时会从回滚段中申请并持有相应的 undo slot。这些 slot 中记录了该事务 Undo 日志链的最后一个节点的位置。回滚时从最后一个节点开始沿着向前指针逐步向前遍历逐一取出该事务产生的 Undo 记录。第二步解析 Undo 记录并生成反向操作。InnoDB 读取每条 Undo 记录的格式识别其类型INSERT 或 UPDATE解析出主键、被修改列、旧值等信息然后在内存中构造对应的反向操作。如果是 Insert Undo反向操作是删除该行如果是 Update Undo反向操作是把旧值写回对应列并同步处理可能涉及的二级索引。第三步逐条执行反向操作。按照与事务执行相反的顺序依次将这些反向操作应用到数据行上。需要注意的是执行反向操作本身也会产生新的数据变更。但这里有一个重要区别回滚过程中产生的这些变更不需要再写入新的 Undo Log。因为在 InnoDB 的设计中事务一旦进入回滚阶段其回滚进度由已知的 Undo 链唯一确定即使回滚中途再次发生崩溃恢复时也只需依据原有 Undo Log 继续完成回滚而无需依赖回滚动作自身的新日志。第四步同步维护锁与状态。在回滚过程中之前由该事务持有的行锁、间隙锁等会随着对应操作的撤销而逐步释放。回滚全部完成后事务状态被标记为完全回滚相关资源被回收undo slot 被标记为可复用。6.3 回滚与 Redo Log 的关系一个经常被问到的问题是既然有 Redo Log 保证崩溃后数据的持久化为什么回滚还需要 Undo Log两者会不会冲突答案是两者职责不同、互补而非冲突。Redo Log 解决的是「已提交数据在崩溃后如何重放恢复」它保证的是持久性Durability。Undo Log 解决的是「未提交数据如何撤销」以及「旧版本如何被读取」它保证的是原子性和隔离性。在崩溃恢复时InnoDB 会先利用 Redo Log 重放所有已写入磁盘页的物理操作把数据库推进到崩溃前的物理状态随后再扫描 Undo Log找出崩溃时尚未提交的事务对它们执行回滚。这个过程通常被称为「redo 重做 undo 撤销」。两者先后配合最终使数据库达到一致状态。更进一步讲Undo Log 本身在被写入时也会产生 Redo Log因为 Undo 页和普通数据页一样都是内存中的页对象都需要通过 Redo 机制保证其持久性。这也解释了为什么 Undo 相关操作频繁时Redo Log 的写入量也会明显增加。七、MVCC 与 Undo Log一致性读的实现原理MVCC 是 InnoDB 实现高并发读写的核心机制而 Undo Log 是 MVCC 得以运转的存储基础。本节将聚焦 MVCC 与 Undo Log 之间的协作关系把「快照读为什么能看到合适的历史版本」这个问题彻底讲清楚。7.1 当前读与快照读首先区分两种读操作。当前读Current Read读取的是数据的最新已提交版本例如SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE以及 UPDATE、DELETE 语句内部对行的读取。当前读通常会加锁直接读取行记录的最新内容。快照读Snapshot Read则不一样。普通的SELECT语句在默认隔离级别下就是快照读它不加锁通过 MVCC 读取符合事务快照的数据版本。快照读看到的不是物理页上的最新行记录而是根据 ReadView 和版本链计算出来的「历史版本」。这里的版本链正是由 Undo Log 串联起来的。7.2 ReadView 的构成与判断规则在可重复读隔离级别下快照读会在事务中第一次读取时生成一个 ReadView。在读已提交READ COMMITTED隔离级别下每次快照读都会重新生成 ReadView。ReadView 主要包含以下关键信息m_ids生成 ReadView 时系统中所有活跃事务尚未提交的事务的事务 ID 列表。min_trx_id活跃事务列表中最小的事务 ID。max_trx_id即将分配给下一个事务的事务 ID 上限。creator_trx_id生成该 ReadView 的事务自身 ID。当一致性读访问一行数据时会拿到该行当前版本的 DB_TRX_ID然后按照以下规则判断这个版本是否可见如果 DB_TRX_ID 等于 creator_trx_id说明该行是当前事务自己修改的可见。如果 DB_TRX_ID 小于 min_trx_id说明修改该行的事务在 ReadView 生成前已经提交可见。如果 DB_TRX_ID 大于等于 max_trx_id说明修改该行的事务在 ReadView 生成后才启动不可见。如果 DB_TRX_ID 位于 min_trx_id 和 max_trx_id 之间则需要进一步判断它是否在活跃事务列表 m_ids 中若在活跃列表中说明该事务尚未提交不可见若不在则说明已经提交可见。如果当前行版本的 DB_TRX_ID 对 ReadView 不可见InnoDB 就会沿回滚指针找到上一版本的 Undo 记录取出其中的旧值及更早的回滚指针继续按同样规则判断直到找到第一个可见版本或者到达版本链末端判定该行完全不存在为止。这个查找过程就是「快照读 Undo 版本链」的完整工作流程。7.3 可重复读与读已提交的差异借助 ReadView 的生成时机可以非常清晰地解释两种隔离级别在快照读上的行为差异。在可重复读级别下ReadView 在事务的第一次快照读时生成并持续作用于整个事务期间。因此事务内后续的所有快照读都使用同一个 ReadView。即使其他事务在此期间提交了修改这些修改对应的 DB_TRX_ID 对旧 ReadView 而言仍然是「不可见」的所以事务内反复读取同一行会得到相同的结果实现了可重复读。在读已提交级别下每次快照读都会重新生成 ReadView。每次生成的 ReadView 都会反映最新的已提交事务集合。因此一个事务在前后两次普通 SELECT 之间如果其他事务提交了修改第二次 SELECT 就能看到新的数据。这正是「读已提交」得名的原因。值得注意的是MySQL 的 MVCC 通过一致性读解决了大多数幻读问题尤其在可重复读级别下普通快照读不会出现幻读。但对于当前读如加锁的 SELECT FOR UPDATE可重复读级别仍需依靠间隙锁Gap Lock来防止对同一范围的并发插入才能完整地防住幻读。这个细节与本节的 Undo 主题相关度有限但有助于澄清「MVCC 是否彻底解决幻读」这一常见疑问。八、Purge 机制Undo Log 的生命周期终结Undo Log 不是永久存在的。它从产生到消亡要经历「写入、提交、等待、清理」四个阶段。其中最后一个阶段由后台的 Purge 机制完成。Purge 机制的健康程度直接决定了 Undo 空间是否会无限膨胀、历史版本链是否会无限拉长。8.1 为什么不能立即删除 Undo Log表面上看一个事务提交之后它的修改已经生效对应的 Undo Log 似乎就可以删除了。但问题在于系统中可能还存在其他更早开启的只读事务它们的 ReadView 仍然把这些已经提交的修改视为「不可见」从而需要沿着版本链访问更旧的数据。如果在这些事务结束之前就删除 Undo 记录它们的后续一致性读就会找不到正确的历史版本发生数据不一致。因此Undo Log 的清理必须满足一个条件没有任何活动的 ReadView 还需要依赖这条 Undo 记录。用更形式化的语言描述就是一个事务产生的 Undo 记录只有在所有可能需要读取其历史版本的事务都已结束后才能被清理。Purge 线程负责做这样的全局判断并批量执行清理。8.2 Purge 的工作流程Purge 线程的工作大致可以分为以下几个步骤第一步确定可清理边界。系统会维护一个「最老活跃 ReadView」的快照。对于每个 Undo 记录判断其对应事务是否已经被提交并且其事务 ID 是否早于最老活跃 ReadView 的可见边界。满足条件的记录才是可清理的候选。第二步批量读取 Undo 页面。Purge 线程按照回滚段和 Undo 页的顺序批量加载候选的 Undo 记录而不是随机地逐条读取。批量处理可以减少 I/O 次数提高清理效率。第三步解析并执行清理。对每一条候选记录根据其类型执行相应动作。对于 Insert UndoPurge 会删除其在索引中的对应记录因为插入的事务已经提交且没有任何快照需要看到它对于 Update UndoPurge 需要更新索引中记录的版本链指针把链头指向更早的版本使这条中间版本不再被引用。第四步释放 Undo 空间。当一个 Undo 页上的所有记录都被清理后这个页就可以被回收。对于独立 Undo 表空间当达大文件缩容条件时还会触发在线截断truncate把表空间物理文件缩小释放磁盘空间。可以看到Purge 实际上承担了两种看似相反的工作它既要删除 Insert Undo 对应的记录也要修复 Update Undo 的版本链。理解这两类清理动作的差异有助于分析 Purge 延迟时数据库表现出的具体症状。8.3 Purge 延迟的常见原因与影响在实际运维中Purge 延迟是一个高频问题。所谓 Purge 延迟是指产生 Undo Log 的速度超过了 Purge 清理的速度导致 Undo 空间持续增长、版本链越来越长。造成 Purge 延迟的常见原因包括存在长时间未提交或长时间存活的只读事务其旧 ReadView 卡住了可清理边界导致大量 Undo 无法清理。大量热行被高频更新版本链过长。虽然单个版本链的追溯是计数的但过长链条会明显拖慢一致性读的性能。Purge 线程数量不足或负载过高。默认 4 个 Purge 线程在面对高写入压力时可能成为瓶颈。二级索引多、数据量大导致 Purge 清理索引条目成本高。磁盘 I/O 能力不足Purge 批量读写遭受到瓶颈。Purge 延迟带来的影响是多方面的磁盘空间被 Undo 表空间占用一致读需要回溯更长的版本链查询变慢数据库运行一段时间后整体性能下降。因此监控 Purge 延迟应当是 DBA 日常巡检的重要组成部分。九、Undo Log 与 Redo Log、Binlog 的协同单独看 Undo Log 已经足够复杂而它真正发挥作用时往往是和 Redo Log、Binlog 协同工作的。把这三者的关系梳理清楚有助于建立完整的 InnoDB 日志认知。9.1 一次 UPDATE 语句的完整日志旅程以一条简单的 UPDATE 语句为例看看它在 InnoDB 内部的日志流转过程UPDATE users SET balance balance - 100 WHERE id 888;假设这条语句在一个显式事务中执行以下是简化后的一次执行流程第一步定位并加锁。InnoDB 根据主键找到目标行加上排他锁读取当前行版本。第二步写入 Undo Log。在内存中构造该行的旧值生成一条 Update Undo 记录并把它写入回滚段中的 Undo 页。同时更新行记录的 DB_ROLL_PTR 指向这条 Undo 记录。写入 Undo 页的过程中还会同步生成相应的 Redo Log以保护 Undo 页的持久性。第三步修改数据页并写 Redo Log。修改数据页内存中的行记录随后把数据页的物理变更写入 Redo Log。此时数据页在内存中的新版本尚未刷盘但 Redo Log 记录了所有必要的物理信息。第四步事务提交。提交时触发 Redo Log 的刷盘。对于开启 Binlog 的场景提交阶段还会先写 Binlog再执行 InnoDB 的两阶段提交确保两者一致。第五步Post-commit 处理。事务提交后其 Undo 记录进入待 Purge 状态。Insert Undo 可立即清理候选Update Undo 则等待所有相关旧 ReadView 结束后由 Purge 清理。从这条链路可以清晰地看出Undo Log、Redo Log 与 Binlog 各司其职Undo Log 负责提供旧版本、支撑回滚与 MVCCRedo Log 负责崩溃时重放物理修改Binlog 负责归档、复制与基于时间点的恢复。它们在事务的各个阶段紧密衔接共同保证了 ACID 与高可用能力。9.2 崩溃恢复中的协作崩溃恢复Crash Recovery时InnoDB 的日志协作表现得最为集中。数据库异常宕机后的启动过程大致如下扫描 Redo Log重放其中所有已记录但可能未落盘的物理修改使数据页恢复到崩溃时刻的状态。扫描 Undo Log识别出崩溃时所有尚未提交的事务。对这些未提交事务执行回滚撤销它们在数据页上的修改并清理相关锁与状态。对已经提交但尚未完成 Purge 的事务恢复 Purge 边界信息交由后台线程继续清理 Undo。从这个过程可以看出「先 redo、后 undo」的顺序是保证数据正确的关键。如果顺序颠倒先回滚再重放就可能导致已提交事务的修改被错误地撤销或者未提交事务的修改残留下来。InnoDB 通过对 Redo Log 和 Undo Log 的精确顺序控制保证了无论何时崩溃数据库都能在重启后收敛到一个一致状态。十、Undo Log 的监控与诊断对开发者而言理解原理固然重要但在生产环境中能够快速定位 Undo Log 相关的问题往往更有实际价值。本节介绍一些常用的监控指标、系统视图与诊断方法。10.1 关键监控指标以下指标可以帮助我们判断 Undo Log 与 Purge 的健康状况History list length当前尚未被 Purge 的 Undo 记录数量。这个值越高说明 Undo 积压越严重通常也意味着版本链越长。Undo 表空间大小观察文件是否持续膨胀以及是否出现无法截断的情况。Purge 线程繁忙度通过SHOW ENGINE INNODB STATUS查看 Purge 是否持续处于忙碌状态。长事务数量与时长长时间存活的事务是卡住 Purge 的主要元凶。其中 History list length 是最直观的指标通常可以通过以下查询在 information_schema 中获取SHOW ENGINE INNODB STATUS\G该命令输出的 TRANSACTIONS 段中会包含类似如下的信息History list length 287451当 History list length 持续增长且长时间不下降时就应该立即排查是否有长事务存在。10.2 定位长事务排查长事务最常用的系统表是information_schema.innodb_trx。通过该表可以查看当前所有活动事务的开始时间、事务状态、执行的 SQL 等信息。例如下面的查询可以按事务开始时间排序找出运行时间最长的几个事务SELECT trx_id, trx_state, trx_started, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration_seconds, trx_mysql_thread_id, trx_query FROM information_schema.innodb_trx ORDER BY trx_started ASC LIMIT 10;如果查到的某个事务只执行了普通 SELECT 却长时间不结束它可能持有旧 ReadView正在卡住 Purge。虽然普通 SELECT 不加行锁但它对一致性读的依赖会直接影响 Undo 的清理边界。这类事务需要与应用侧配合及时发现并结束无意义的长时间连接。10.3 查看 Undo 表空间状态在 MySQL 8.0 中可以通过information_schema.innodb_tablespaces查看 Undo 表空间的状态信息。与文件系统层面观测文件大小结合可以判断某个 Undo 表空间是否处于活跃截断状态、是否出现异常膨胀。另外SHOW STATUS LIKE Innodb_%中的若干计数器也值得关注例如与 Undo 相关的回滚段使用情况、Purge 处理批次数等。虽然单个计数器不能直接定位问题但把它们与 History list length、磁盘空间、事务时长等指标结合起来能够形成相当清晰的诊断图景。十一、Undo Log 相关的性能优化实践理解原理与诊断方法之后本节聚焦于优化实践。优化 Undo Log 相关性能目标通常有两个一是减少 Undo Log 对磁盘空间和 I/O 的压力二是避免长版本链拖慢一致性读。11.1 合理设置 Undo 表空间在部署阶段就应该为 Undo 表空间预留合理的容量。如果一个实例的写入压力很大默认的两个 1GB 上限表空间可能很快会被撑满并频繁触发截断。此时可以提前创建多个 Undo 表空间并适当调大innodb_max_undo_log_size。不过调大上限并不意味着可以无限放任 Undo 生长Promote 及时清理才是根本。关于硬盘类型强烈建议将 Undo 表空间存放在高 IOPS 的 NVMe SSD 上。由于 Undo 的写入是随机的、高频的且 Purge 也会产生随机读与写机械硬盘很容易成为整个系统吞吐的瓶颈。如果条件允许把 Undo 表空间与数据文件、Redo Log 分开部署可以进一步降低 I/O 争用。11.2 控制事务粒度与时长长事务是 Undo 膨胀、Purge 卡住、版本链过长的最常见根因。优化 Undo 相关性能首要任务就是缩短事务时长、控制事务粒度。可以从以下几个方面入手避免在事务中执行耗时的业务逻辑、远程调用、用户交互等操作。大批量数据变更时根据业务容忍度合理拆分事务例如每处理 1000 行提交一次。对只读查询及时关闭连接或提交事务避免旧 ReadView 长时间驻留。使用监控告警手段自动发现并终止超过阈值的长事务。事务越短产生 Undo 后能被 Purge 掉的速度就越快版本链也越短一致性读的代价就越小。11.3 调整 Purge 并发度在写入压力较高的场景下适当增加 Purge 线程数量可以提升清理速度。参数innodb_purge_threads默认是 4最大可设置为 32。需要注意的是并不是 Purge 线程越多越好。Purge 线程增多会消耗更多 CPU 和 I/O如果系统资源本身紧张反而可能影响前台业务。建议结合实际负载通过观察 History list length 的变化趋势来逐步调整。此外innodb_purge_batch_size控制每批从历史链中读取的 Undo 页数量。默认 300 对大多数场景是合理的。只有在确认 Purge 明显跟不上写入、且 I/O 能力足够时才考虑适当调大该值。11.4 避免不必要的海量 DML某些业务模式天然会产生大量 Undo例如高频对同一批行做无差别更新、大量整表删除后重新插入等。如果能从业务层面减少不必要的 DML往往比任何参数调优都更有效。例如对于整表数据重算优先考虑批量重建而非逐行 UPDATE。对于需要清空表数据的场景在条件允许时使用TRUNCATE代替DELETE。TRUNCATE 在 InnoDB 中是 DDL 操作通过重建表空间释放空间不会产生大量行级 Undo因此效率远高于逐行 DELETE。合理利用「插入后更新」的批处理策略减少重复修改同一行的次数。11.5 定期处理历史表对于历史数据归档而言如果每次都通过在事务里大量 DELETE 来清理过期数据会长时间占用 Undo 空间并产生沉重的 Purge 负担。更优的做法是按分区进行归档删除或者采用分区交换、备份后 TRUNCATE 分区等方式以更小的 Undo 代价完成大范围数据清理。综合而言Undo 相关性能优化的核心思想可以归纳为一句话让 Undo 记录尽快变得不再被需要并让 Purge 有能力及时把它们清走。一切参数调整都应围绕这个目标展开。十二、常见误区与疑难问题解惑在对 Undo Log 有了系统和深入的了解之后再来审视一些常见误区会更有体会。本节挑选了若干开发者与 DBA 经常问到的问题逐一进行分析和澄清。12.1 「Undo Log 在磁盘上回滚是不是从磁盘读取日志」不完全是。Undo Log 所在页也同普通数据页一样遵循 InnoDB 的缓冲池Buffer Pool管理机制。事务执行时Undo 记录首先写入内存中的 Undo 页再由后台刷盘机制异步落盘。回滚时优先从缓冲池读取相关 Undo 页若页已被淘汰则从磁盘读入内存。因此回滚的代价并不等同于每次都触发磁盘读缓冲池的命中率会显著影响回滚性能。这也是为什么内存配置不足、缓冲池频繁换出时回滚可能变得很慢。12.2 「Undo Log 越大回滚越快」恰恰相反。Undo 记录越多、版本链越长回滚与一致性读的代价通常越高。对于回滚而言一个执行了大量操作的事务其回滚需要逐条撤销所有操作工作量和操作数量成正比。对于一致性读而言版本链越长回溯的成本越高。因此从性能角度看应尽量让 Undo 保持精简而不是追求「大」。不过对于 Purge 迟迟无法推进、Undo 大量积压的情况磁盘空间足够大确实能避免事务因空间不足而失败。所以准确的说法是需要为 Undo 预留充足空间以避免运行故障但持续的 Undo 膨胀本身是性能隐患信号。12.3 「DELETE 删除的数据马上就不占空间了吗」不是。DELETE 只是把数据标记为删除并在 Undo 中写入一条 Update Undo 记录。真正把数据从物理上清理掉的动作由 Purge 完成后才发生对于页内已删除的行可能还要等页整理或重建才最终释放。因此DELETE 之后表空间文件通常不会立刻变小这是正常现象。除非使用 TRUNCATE 或在线重建表等手段否则表空间大小不会因为 DELETE 而立即收缩。12.4 「READ COMMITTED 就不需要 MVCC 了吗」需要而且同样依赖 Undo Log。读已提交只是每次快照读都生成新 ReadView而不是放弃使用一致性读。它仍然需要通过版本链找到满足 ReadView 可见性的历史版本。只不过由于每次读都刷新视图旧版本被需要的时间窗口更短Purge 可以更积极地清理。从 Undo 清理角度看读已提交通常比可重复读更「轻快」。12.5 「关闭 Binlog 后 Undo Log 就不再需要了」错误。Binlog 与 Undo Log 是两个独立体系前者位于服务层后者位于 InnoDB 存储引擎层。即使关闭 BinlogInnoDB 仍然需要 Undo Log 来保证事务回滚与 MVCC两者没有替代关系。关闭 Binlog 只会影响复制、归档与时间点恢复能力并不会让 InnoDB 的事务机制退化。12.6 「History list length 为 0 就代表没有 Undo 了」History list length 为 0 说明当前没有尚未清理的已提交事务的 Undo 记录但这不代表 Undo 表空间文件大小为 0。因为页面的分配与回收是批量进行的即使所有记录都已清理已分配的 Undo 页也可能尚未被释放或表空间尚未截断。表空间大小的下降通常滞后于 History list length 的下降。十三、实战案例一次典型的长事务导致的 Undo 膨胀为了把这些知识串起来本节通过一个虚构但贴近真实场景的案例完整演示一次 Undo 膨胀的发现、分析与处理过程。13.1 现象某业务数据库出现响应变慢监控显示查询延迟升高同时磁盘使用率在短时间内从 40% 快速上升到 80%。DBA 登录实例后执行SHOW ENGINE INNODB STATUS发现如下关键信息History list length 1520367 ... Trx id counter 2090123 Purge done for trxs n:o 1980345 undo n:o 0History list length 已经超过 150 万说明有大量 Undo 记录积压未被清理。同时磁盘空间告急初步判断为 Undo 表空间膨胀所致。13.2 分析进一步查询information_schema.innodb_trx发现存在一个已经运行超过 2000 秒的事务它的trx_state为 RUNNINGtrx_query显示为一个简单的 SELECT 查询SELECT * FROM orders WHERE status PENDING;这个事务本身并没有修改大量数据但它长时间没有提交一直持有一个旧的 ReadView。与此同时业务侧有大量的订单状态更新在持续提交。由于这些更新发生在该旧 ReadView 之后它们对应的 Undo 记录无法被 Purge因为那个长时间存活的只读事务「理论上」还需要看到更新前的旧版本。于是大量 Undo 被卡住History list length 持续上涨。13.3 定位与处理DBA 通过trx_mysql_thread_id关联到进程列表找到了这个长事务对应的应用连接。经过与应用方确认该连接源自一个忘记显式提交或关闭的 ORM 会话。处理方式如下立即通过KILL结束该连接使旧 ReadView 失效解除 Purge 的阻塞。观察 History list length确认它开始下降。排查应用侧的连接管理修复「查询后不提交、连接长期挂起」的问题。增加针对事务「运行时间超过阈值」的监控告警防止类似问题再次发生。13.4 后续优化在解决本次故障后团队还采取了如下长期优化措施将只读查询从写事务链路中剥离尽量使用自动提交模式对于必须保持开启的事务严格缩短其生命周期。完善长事务监控与自动告警将阈值设置为 30 秒。根据业务写入量评估 Undo 表空间容量适当增加 Undo 表空间数量并调整innodb_max_undo_log_size。定期演练磁盘空间不足的应急预案包括使用在线 Undo 表空间截断、清理历史分区数据等手段。这个案例虽然简单却完整覆盖了「识别症状、分析指标、定位根因、执行处置、长期加固」的故障处理闭环。它提醒我们很多所谓的 Undo 问题根源并不在 Undo Log 本身而在事务的生命周期管理。十四、Undo Log 的未来演进与最佳实践总结任何技术都不是一成不变的。了解 Undo Log 的现状与演进方向有助于在面对未来版本升级时做出更从容的判断。14.1 版本演进回顾回顾 Undo Log 的版本演进可以看到一条清晰的趋势从共享走向独立从不可收缩走向在线管理从经验调优走向可观测、可控制的精细化运维。MySQL 5.5 及以前Undo 存放在系统表空间管理粗糙。MySQL 5.6开始支持独立的 Undo 表空间但存在若干限制。MySQL 5.7进一步增强了 Undo 表空间的数量控制与在线截断能力。MySQL 8.0独立 Undo 表空间全面成熟支持动态创建、删除、标记不活跃和在线 shrink回滚段数量默认提升至 128Purge 可配置性增强。14.2 最佳实践清单结合全篇内容这里给出一份可操作的 Undo Log 最佳实践清单供开发与运维人员参考部署阶段使用 MySQL 8.0 的独立 Undo 表空间将 Undo 目录放在高性能 SSD 上根据写入负载预留空间合理设置innodb_max_undo_log_size。开发阶段严格控制事务粒度与时长避免在事务中执行远程调用和慢业务逻辑对大批量 DML 采取分批提交策略能用 TRUNCATE 就不用 DELETE 清理整表。运维阶段监控 History list length、Undo 表空间文件大小、长事务时长和磁盘使用率设置阈值告警定期检查information_schema.innodb_trx。优化阶段当发现 Purge 延迟时先排查长事务源头再评估是否需要增加 Purge 线程避免盲目调大innodb_purge_batch_size关注低效 DML 的业务优化。容量规划将 Undo 空间纳入容量模型预留至少能容纳「最长可能事务窗口内产生的最大 Undo 量」的空间并预留足够的剩余磁盘空间。14.3 结语如果把 MySQL 比作一台精密的机器那么 Undo Log 就是这台机器中那一组看似不起眼却至关重要的逆转齿轮。它让已经发生的数据修改可以安全地被撤销让尚未开始的读事务可以稳定地看到过去也让不同生命周期的事务得以在同一套存储结构上和谐共处。真正理解了 Undo Log才能理解事务回滚为什么可靠理解 MVCC 为什么高效也才能在生产环境中对性能与容量做出更准确地判断。本文从概念、存储、结构、回滚、MVCC、Purge、日志协同、监控、优化、疑难问题和实战案例等多个维度对 MySQL InnoDB 的 Undo Log 进行了系统而深入的解析。希望读者能够将这些知识转化为实际的开发规范与运维策略在日常工作中少走弯路更从容地应对与事务和并发相关的各类挑战。技术海洋广阔Undo Log 只是其中一块砖石。但它所体现的「用可控的冗余换取可靠的回退与并发能力」这一思想贯穿于几乎所有现代数据库与分布式系统的设计之中。愿每一位读到这里的同行都能透过这块砖石看见更广阔的工程之美。
返回列表