原子性指的是事务要么完全成功执行,要么完全失败回滚,不允许部分执行。

这本质上是在要求具有rollback 回滚能力

InnoDB中的事务可能会由用户主动触发Rollback;也可能因为遇到死锁异常Rollback;或者发生Crash,重启后对未提交的事务回滚。

InnoDB 的 rollback回滚能力 是基于 undo log 实现的。undo log 记录了修改操作前的旧版本数据,以便在回滚时恢复数据。

1 一条 undo log 的结构

1.1 undo log 的分类

只有在事务中对数据进行修改(如 INSERT、DELETE、UPDATE)的时候, 才需要记录undo log,快照读 select 不需要记录。

不同的修改操作产生的 undo log 记录的内容和结构会有所不同,因为每种操作对数据的影响不同, 所以undo log 也会有不同的类型。

InnoDB 的 undo log 主要分为两大类:

  1. TRX_UNDO_INSERT:此类主要包括 TRX_UNDO_INSERT_REC 类型的日志,专门用于记录插入操作的撤销信息。
  2. TRX_UNDO_UPDATE:此类包括 TRX_UNDO_UPD_EXIST_RECTRX_UNDO_DEL_MARK_REC,用于记录更新存在的记录和标记删除操作的撤销信息。
  • 共同点:所有类型的操作都需要且只需要记录足够的信息来逆转所执行的操作。这些记录都存储在 InnoDB 的 undo 表空间或者系统表空间中
  • 差异:不同操作类型的 undo 日志记录的具体内容根据操作的性质而异。INSERT 主要关注标记新增行的删除,DELETE 需要记录完整的行数据以便恢复,而 UPDATE 记录修改前的字段值。

⚠️:对于 undo log 的记录并不是基于每条修改 SQL 语句,而是基于 修改SQL 语句影响的每一条记录。这意味着每条被修改的记录都会有对应的 undo log 。如果一个 SQL 语句影响修改了多行数据,那么将会有多条 undo log 生成。

1.2 INSERT 操作的 Undo log

对于 INSERT 操作,undo 日志通常记录较少的信息,主要是把这条记录的主键信息记上。

1.2.1 end of record 和 start of record

在InnoDB的undo日志结构中,end of recordstart of record 两个字段共同起到链接undo日志记录的作用,使这些记录形成一个双向链表,并提供顺序遍历和反向遍历的功能

end of record

  • 定义:
    • end of record字段指示当前undo日志记录的结束位置,并提供下一条undo日志记录的起始地址。
    • 当最后一条undo日志记录没有后继时,则下一条undo日志记录的起始地址为NULL
  • 目的:
    • 指向链表中的下一条记录,方便顺序遍历日志记录,可以用于回放或者重做日志,特别是在恢复阶段

start of record

  • 定义:
    • start of record字段指示当前undo日志记录的起始位置,并提供上一条undo日志记录的结束地址。
    • 如果当前undo日志记录是链表中的第一条,则上一条undo日志记录的结束地址为NULL
  • 目的:
    • 指向链表中的上一条记录,方便反向遍历日志记录,用于事务回滚

1.2.2 undo type

该字段指定undo日志记录的类型, 用于区分不同类型的undo操作,如TRX_UNDO_INSERT_RECTRX_UNDO_UPD_EXIST_RECTRX_UNDO_DEL_MARK_REC

1.2.3 undo no

日志编号, 在一个事务内从0开始递增,每生成一条日志,undo no 就加1

1.2.4 table id

原始记录所在表的标识符,使undo日志能够与原始记录所在的表关联。

1.2.5

此部分以<长度,值>的形式保存每个主键列的信息,以便在回滚插入操作时恢复主键值:

  1. len:表示对应列的存储空间大小。
  2. value:存储主键的实际值。

1.3 DELETE 操作的 Undo log-标记删除

  1. trx_id
    记录上一个旧版本数据的trx_id, 该值从行记录的隐藏列trx_id 中获取
    • 目的: 在回滚过程中,这个字段可以帮助恢复被删除的记录的原始事务信息,确保在恢复期间不会出现不一致的问题。
  2. roll_pointer
    记录上一个旧版本数据的roll_pointer, 该值从行记录的隐藏列roll_pointer 中获取
    • 描述: 指向被删除记录的原始回滚指针 (roll_pointer)。

在InnoDB中,每条行记录都有一个位(bit)标记来指示该记录的状态,包括是否已被删除。这是通过记录头(Record Header)中的info bits字段实现的。

1.3.1 标记删除

TRX_UNDO_DEL_MARK_REC 日志 指的是对记录的逻辑删除,逻辑删除指的是只被标记为删除状态,并不会立即将其物理删除,因为还要支持事务回滚,以及MVCC。

被标记删除的数据如果真的需要删除,会在适当的时候由后台线程实际清理

1.3.2 行记录的删除标记

每条行记录的头部都有一个info bits字段,用来存储记录的状态信息,包括是否已被删除。

info bits字段的第5个bit位上,标记记录是否已被删除, 当此bit位为1时,表示该记录已被标记删除;为0时,表示该记录是正常的。

1.4 UPDATE 操作的 Undo log

update 的操作 分为两种

  1. 不更新主键的update, 这种操作 一条记录 只会TRX_UNDO_UPD_EXIST_REC 一条undo log

  2. 更新主键的update ,这种update在实际执行时, 会先删除旧记录,再insert 一条新纪录, 所以会记录两条undo log, 一条TRX_UNDO_DEL_MARK_REC, 一条TRX_UNDO_INSERT_REC

具体字段信息和前面两种类似,不再详述。

2一个事务中的多条undo log如何组织在一起

2.1 undo page -分类型存储undo log

InnoDB 对数据的管理是以 page 为单位进行的,undo log 也遵循这一原则,即存储在专门的 undo pages 中。

每个 undo page 中的日志记录是专用的,不同类型的undo log 不能混着存储, 即一个 page 中不能同时记录 TRX_UNDO_INSERT 类型和 TRX_UNDO_UPDATE 类型的日志。

这样设计的理由是为了避免在回滚时需要在同一页面上搜索不同类型的日志记录,从而提高了回滚操作的效率。

可是 一个事务内可以同时存在insert undo log和update undo log, 如果事务需要回滚则所有操作都需要回滚,那为什么还要分开存储呢?

  1. 优化事务回滚的逻辑

    • 操作依赖性减少:插入操作的回滚仅涉及到删除之前插入的行,而更新或删除操作的回滚需要恢复原始数据。将这些操作的日志分开,可以在回滚时减少对不同类型日志处理逻辑的依赖,使得回滚过程更加模块化和有序。
    • 执行效率:分开存储使得处理各自的回滚逻辑时可以更加高效,因为每种类型的回滚处理只需关注其对应类型的日志页。这减少了在单一日志页中搜索和处理不同类型日志的复杂性和时间。
  2. 并行处理
    尽管一个事务中可能存在多种类型的 undo 日志,但在并发环境中,不同的回滚任务可能由不同的系统进程或线程处理。例如,某些情况下系统可能并行地处理 insert undo logupdate undo log。分开存储可以减少锁的竞争和管理的复杂性,提高并发处理的效率。

  3. 空间和性能管理
    • 简化空间回收:在事务提交后,insert undo log 可以立即被丢弃和回收,因为插入操作生成的记录一旦提交即视为有效。而 update undo log 可能需要被保留以支持其他事务的一致性读(由于 MVCC)。分开存储使得空间管理更为高效,因为可以针对性地处理和回收日志空间。
    • 优化读取性能:在事务处理过程中,尤其是在一些只涉及到特定类型操作的查询或回滚操作中,分开存储可以优化日志的读取性能,因为系统可以直接定位到相关类型的日志页。
  4. 日志维护的简化
    • 分开存储有助于简化日志维护和日志生命周期管理。系统可以更容易地追踪和管理不同类型日志的生成、使用和清理周期。

2.2 undo page 链表

在一个事务中,可能会产生多条 undo log。

如果一个 undo page 填满了,事务会向系统申请新的undo page,并将其通过链表(通常是使用类似于前驱(previous)和后继(next)指针的机制)连接起来。

前面提到过,一个 undo page 不能混合存储不能类型的链接, 所以对于一个事务它可以有insert undo page 和update undo page 两个链表。

每个事务都会分配单独的页面链表。

下面简单介绍下链表中第一个 undo page

  1. file header 如前面介绍,会有一个字段来标识该page 是undo page, 用来存储undo log。

  2. undo page header 会记录该页面存储的undo log 类型, insert or update

  3. undo log segment header , 会记录该链表所属的segment

  4. undo log header ,理论上,每个事务都会分配自己的页面链表, 但如果一个事务产生的undo log很少,那么这个页面链表就有可能被重用。所以实际上一个页面链表中实际可能存储多个事务的undo log, undo log header 中记录了不同事务间日志的分隔信息。

2.3 回滚段

InnoDB默认创建128个回滚段(Rollback Segments),用于管理undo日志。

  • 元数据存储: 每个回滚段的元数据存储在系统表空间第5号页面中。
  • Slot结构:每个回滚段包含1024个slot,每个slot可以映射到一个Undo页。
  • 事务与回滚段的关联: 事务会在需要的时候分配一个回滚段。
  • 轮询策略: InnoDB使用轮询方式将回滚段分配给新事务,以实现负载均衡。

2.4 Undo页链表的形成与维护

  1. 事务开始:
    • 新的事务开始时,会分配一个插入段和一个更新段。
    • 在分配的回滚段头页中,初始化undo页链表的头指针和尾指针。
  2. 查找可用的Slot:
    • 事务在开始写入undo日志时,会首先查找一个可用的slot,并初始化一个新的undo页链表。
  3. 分配新的Undo页:
    • 分配新的undo页,将其添加到undo页链表的末尾。
    • 如果这是链表的第一个undo页,回滚段头页的first指针和last指针会同时指向该页。
  4. 维护Undo页链表:
    • 当undo页链表中的最后一个undo页已满时,分配一个新的undo页并链接到链表的末尾。
    • 回滚段头页的last指针会指向新分配的undo页。
    • 新undo页的prev指针指向链表的前一个undo页,形成链表结构。

3 行记录如何与undo log 关联 -roll_pointer

roll_pointer 是存储在每个行记录中的一个指针,指向该行记录相关的最近一次undo log 记录。

注意这个undo 记录指的是具体的 undo log,而不是整个页面链表。

  • 当行记录被修改(包括更新、删除或作为多步操作的一部分的插入)时,InnoDB 首先会在 undo 日志中写入一条记录,这条记录包含了行修改前的数据,和行记录中的的roll_pointer,
  • InnoDB 更新行记录中的 roll_pointer,使其指向新写入的 undo 日志记录。如果这个行再次被修改,新的 undo 日志将被写入,roll_pointer 会更新为指向这条新的记录。新的undo 日志中会记录之前的roll_pointer

4. 一条记录的版本链如何形成

InnoDB 通过 roll_ptr 把每一行的历史版本串联在一起

  1. 行记录中有roll_pointer,
  2. update 操作中TRX_UNDO_UPD_EXIST_REC 和 TRX_UNDO_DEL_MARK_REC 类型的undo log 中,也有roll_pointer,
    通过这些roll_pointer, 可以形成一条行记录的版本链。

insert 操作中,对应的undo log没有roll_pointer 属性,因为insert 操作就是一个行记录的初始版本,没有比它更早的操作了。

以下是一个通过roll_pointer 组成的版本链,每个undo log 进行了内容省略以展示链接的重点内容

5. undo log 的持久化

undo日志刷盘时机的参数,但通过控制Redo日志、脏页刷新和Purge线程的参数,可以间接影响undo日志的刷盘策略。

WAL技术
在数据实际修改前,先将undo日志持久化到磁盘。

刷盘时机:

  • 事务提交: 当事务提交时,相关的undo日志会被写入磁盘。
  • 脏页刷盘: 在InnoDB将脏页(dirty page)写入磁盘之前,首先会确保所有相关的undo日志已经被持久化。
  • Redo日志同步: 当一个Redo日志被同步到磁盘时,所有相关的undo日志也必须被同步。

6 基于undo log 的回滚操作

InnoDB中的事务

  1. 可能会由用户主动触发Rollback;
  2. 也可能因为遇到死锁异常Rollback;
  3. 或者发生Crash,重启后对未提交的事务回滚。

    6.1. 用户/应用程序主动回滚

  • 反向遍历(start of record)当前事务的undo日志链表,按逆序恢复每个更改。
  • 插入操作: 在数据页中删除已插入的记录。
  • 删除操作: 恢复已删除的记录。
  • 更新操作: 恢复更新前的记录。
  • 每个操作恢复完成后,从undo日志链表中移除相应的undo日志记录。

6.2. 死锁异常回滚

InnoDB通过死锁检测算法发现两个或多个事务之间的锁等待,形成死锁,,选择最小代价,即持有锁资源最少的事务务进行回滚。

与主动回滚类似,遍历当前事务的undo日志链表,按逆序恢复每个更改。

6.3 崩溃恢复

MySQL服务器或操作系统崩溃后,InnoDB通过Undo日志与Redo日志结合,确保崩溃时数据页的状态恢复到一致的状态, undo日志用来 回滚未提交的事务。

7 undo log 的清理

事务提交后,相关的Undo日志记录仍需保留一段时间以支持多版本并发控制(MVCC)

7.1 Purge 线程

InnoDB 通过一个后台线程称为 Purge,来清理不再需要的 undo log。

  • 触发条件:Purge 进程会定期检查那些已提交事务的 undo log。它会确定这些 undo log 是否还被其他活跃事务作为 MVCC 的一部分所需。
  • 删除操作:如果一个 undo log 记录不再被任何事务所需要,Purge 进程会将其从 undo 表空间中删除,释放相关资源。

undo log 的清理机制是区分操作类型的。

7.2 Insert Undo Log

Insert undo log 主要记录插入操作的信息。因为插入操作仅仅添加新的记录,不涉及已存在数据的修改,所以这种类型的 undo log 主要用于在事务失败时撤销插入操作。

  • 清理时机:当一个事务进行插入操作并成功提交后,相应的 insert undo log 立即变得无用,因为插入的数据已经被确认并不需要再被撤销。此时,这些 undo log 可以被安全地清理掉,因为它们不再被任何事务所需。
  • 清理过程:Purge 线程会检测到这些 insert undo log 与已提交的事务关联,并将它们标记为可清理。然后,这些 log 会从 undo 表空间中删除,相关的磁盘空间得以回收。

7.3 Update Undo Log

Update undo log 记录了对现有数据的修改(包括更新和删除操作)。这些记录对于事务回滚和多版本并发控制(MVCC)至关重要。

  • 清理时机:与 insert undo log 不同,即使相关事务已经提交,update undo log 也不能立即被清理。这是因为在 InnoDB 中实现 MVCC 时,其他并发事务可能需要访问这些 log 中的旧数据版本来维持一致性读。
  • 清理过程:Purge 线程会周期性地检查 update undo log。只有当这些 log 记录不再被任何其他活跃事务所需时(即没有更早的读视图需要这些数据),它们才会被标记为可清理。然后,Purge 操作会逐步从 undo 表空间中删除这些记录。

7.4 长事务对undo log 清理的影响

长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。

相关文章

Intro to 事务
Intro to InnoDB 事务
InnoDB事务-隔离性的实现,MVCC & 锁
InnoDB事务-持久性的实现,binglog & redo log&undo log