Undo Log
什么是Undo Log?
Undo Log是MySQL的三大日志之一,当我们对记录做了变更操作时就会产生一条Undo记录。它的作用就是保护事务在异常发生的时候或手动回滚时可以回滚到历史版本数据,能够让你读取过去某一个时间点保存的数据。通俗易懂地说,它只关心过去的数据。MySQL 的 InnoDB 存储引擎使用 Undo Log 来支持事务处理。InnoDB 是一个支持事务的存储引擎,它依赖 Undo Log 来实现事务的回滚和 MVCC(多版本并发控制)。
UndoLog 主要用于事务的回滚操作。当一个事务开始执行一系列的数据库操作(如插入、更新、删除记录等)时,数据库系统会同时记录相应的 UndoLog。这些日志记录了如何撤销事务已经执行的操作,使得事务在需要回滚时能够将数据恢复到事务开始之前的状态。
例如,假设有一个事务 T1,它将表中的某一行数据的某个字段从值 A 更新为值 B。在执行这个更新操作的同时,数据库会在 UndoLog 中记录下 “将该字段从值 B 恢复为值 A” 的操作步骤。如果事务 T1 由于某种原因(如出现错误或者被显式地要求回滚)需要回滚,数据库系统就可以利用 UndoLog 中的记录来撤销更新操作,将数据恢复到原来的状态。
同时,undo log 的信息也会被记录到 redo log 中,因为 undo log 也要实现持久性保护。并且,undo-log 本身是会被删除清理的,例如 INSERT 操作,在事务提交之后就可以清除掉了;UPDATE/DELETE 操作在事务提交不会立即删除,会加入 history list,由后台线程 purge 进行清理。
undo log 是采用 segment(段)的方式来记录的,每个 undo 操作在记录的时候占用一个 undo log segment(undo 日志段),undo log segment 包含在 rollback segment(回滚段)中。事务开始时,需要为其分配一个 rollback segment。每个 rollback segment 有 1024 个 undo log segment,这有助于管理多个并发事务的回滚需求。
通常情况下, rollback segment header(通常在回滚段的第一个页)负责管理 rollback segment。rollback segment header 是 rollback segment 的一部分,通常在回滚段的第一个页。history list 是 rollback segment header 的一部分,它的主要作用是记录所有已经提交但还没有被清理(purge)的事务的 undo log。这个列表使得 purge 线程能够找到并清理那些不再需要的 undo log 记录。
MVCC
的实现依赖于:隐藏字段、Read View、undo log。在内部实现中,InnoDB 通过数据行的 DB_TRX_ID
和 Read View
来判断数据的可见性,如不可见,则通过数据行的 DB_ROLL_PTR
找到 undo log 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 Read View
之前已经提交的修改和该事务本身做的修改。
对于一个InnoDB存储引擎,一个聚簇索引(主键索引)的记录之中,一定会有两个隐藏字段trx_id和roll_pointer,这两个字段存储于B+树的叶子节点中,分别对应记录着两列信息:
- trx_id:只要有任意一个事务对某条聚簇索引记录进行修改,该事务id就会被记录到该字段里面。
- roll_pointer:当任意一个聚簇索引记录被修改,上一个版本的数据记录就会被写入Undo Log日志里面
那么这个roll_pointer就是存储了一个指针,这个指针是一个地址,指向这个聚簇索引的上一个版本的记录位置,通过这个指针就可以获得到每一个历史版本的记录。
OK,假设我们现在有一张员工表employee,有主键id、name、age三个字段。使用一个事务A创建第一条主键id为1的记录,把名字修改为fancy,他的年龄为25岁。
那么,这条记录的trx_id隐藏字段就会记录此次插入记录的事务ID:

假设现在有一个事务B,事务ID为20,要对这条记录进行修改,把fancy的age从25改成了28,那么此时Undo Log会发生啥?
此时,这这条id为1的记录,trx_id就变为了20,是的。trx_id此时记录了修改这条记录的事务ID,而对应的roll_pointer指针,就指向了上次事务A的操作对应的Undo Log:

好,所以这里先总结一下:trx_id就是记录修改了每条聚簇索引的事务id;roll_pointer顾名思义就是个指针,指向每一个历史操作版本的数据存储的地址;每一次修改操作都会生成一个Undo Log版本,每个版本之间是隔离的。
如果接下去有事务C,事务D等等一直对这条记录进行修改,那么这条记录的roll_pointer指针就会一直这样递归修改下去,最终形成一个关于修改和删除操作的Undo Log版本链!
为什么我说是关于修改和删除操作的版本链,而没有查询操作的呢?因为,查询操作不会生成Undo Log版本链。
那么,InnoDB有几种版本链呢?其实只有两种:insert undo log(插入操作产生) 和update undo log(更新操作产生)。
就像我开头所说的,读操作和写操作是分离的,它们之间没有关系。写操作去生成版本链,而读操作只需要根据规则去查看对应的某一个版本,然后读取就完事了。至于是什么规则?就是我们下文要说的:Read View。
Read View
什么是Read View?
Read View 存放着一个列表,这个列表用来记录当前数据库系统中活跃的读写事务,也就是已经开启了,正在进行数据操作但是还未提交保存的事务。可以通过这个列表来判断某一个版本是否对当前事务可见。其中,有四个重要的字段:
m_low_limit_id
:目前出现过的最大的事务 ID+1,即下一个将被分配的事务 ID。大于等于这个 ID 的数据版本均不可见m_up_limit_id
:活跃事务列表m_ids
中最小的事务 ID,如果m_ids
为空,则m_up_limit_id
为m_low_limit_id
。小于这个 ID 的数据版本均可见m_ids
:Read View
创建时其他未提交的活跃事务 ID 列表。创建Read View
时,将当前未提交事务 ID 记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。m_ids
不包括当前事务自己和已提交的事务(正在内存中)m_creator_trx_id
:创建该Read View
的事务 ID
在 InnoDB
存储引擎中,创建一个新事务后,执行每个 select
语句前,都会创建一个快照(Read View),快照中保存了当前数据库系统中正处于活跃(没有 commit)的事务的 ID 号。其实简单的说保存的是系统中当前不应该被本事务看到的其他事务 ID 列表(即 m_ids)。当用户在这个事务中要读取某个记录行的时候,InnoDB
会将该记录行的 DB_TRX_ID
与 Read View
中的一些变量及当前事务 ID 进行比较,判断是否满足可见性条件
- 1.如果记录 DB_TRX_ID < m_up_limit_id,那么表明最新修改该行的事务(DB_TRX_ID)在当前事务创建快照之前就提交了,所以该记录行的值对当前事务是可见的
- 2.如果记录 DB_TRX_ID < m_up_limit_id,那么表明最新修改该行的事务(DB_TRX_ID)在当前事务创建快照之前就提交了,所以该记录行的值对当前事务是可见的,跳到步骤 5
- 3.m_ids 为空,则表明在当前事务创建快照之前,修改该行的事务就已经提交了,所以该记录行的值对当前事务是可见的
- 4.如果 m_up_limit_id <= DB_TRX_ID < m_low_limit_id,表明最新修改该行的事务(DB_TRX_ID)在当前事务创建快照的时候可能处于“活动状态”或者“已提交状态”;所以就要对活跃事务列表 m_ids 进行查找
- 如果在活跃事务列表 m_ids 中能找到 DB_TRX_ID,表明:① 在当前事务创建快照前,该记录行的值被事务 ID 为 DB_TRX_ID 的事务修改了,但没有提交;或者 ② 在当前事务创建快照后,该记录行的值被事务 ID 为 DB_TRX_ID 的事务修改了。这些情况下,这个记录行的值对当前事务都是不可见的。跳到步骤 5
- 在活跃事务列表中找不到,则表明“id 为 trx_id 的事务”在修改“该记录行的值”后,在“当前事务”创建快照前就已经提交了,所以记录行对当前事务可见
- 5.在该记录行的 DB_ROLL_PTR 指针所指向的
undo log
取出快照记录,用快照记录的 DB_TRX_ID 跳到步骤 1 重新开始判断,直到找到满足的快照版本或返回空
RC 和 RR 隔离级别下 MVCC 的差异
在事务隔离级别 RC
和 RR
(InnoDB 存储引擎的默认事务隔离级别)下,InnoDB
存储引擎使用 MVCC
(非锁定一致性读),但它们生成 Read View
的时机却不同
- 在 RC 隔离级别下的
每次select
查询前都生成一个Read View
(m_ids 列表) - 在 RR 隔离级别下只在事务开始后
第一次select
数据前生成一个Read View
(m_ids 列表)
MVCC 解决不可重复读问题
虽然 RC 和 RR 都通过 MVCC
来读取快照数据,但由于 生成 Read View 时机不同,从而在 RR 级别下实现可重复读
举个例子:

在 RC 下 ReadView 生成情况
1. 假设时间线来到 T4 ,那么此时数据行 id = 1 的版本链为:

由于 RC 级别下每次查询都会生成Read View
,并且事务 101、102 并未提交,此时 103
事务生成的 Read View
中活跃的事务 m_ids
为:[101,102] ,m_low_limit_id
为:104,m_up_limit_id
为:101,m_creator_trx_id
为:103
- 此时最新记录的
DB_TRX_ID
为 101,m_up_limit_id <= 101 < m_low_limit_id,所以要在m_ids
列表中查找,发现DB_TRX_ID
存在列表中,那么这个记录不可见 - 根据
DB_ROLL_PTR
找到undo log
中的上一版本记录,上一条记录的DB_TRX_ID
还是 101,不可见 - 继续找上一条
DB_TRX_ID
为 1,满足 1 < m_up_limit_id,可见,所以事务 103 查询到数据为name = 菜花
2. 时间线来到 T6 ,数据的版本链为:

因为在 RC 级别下,重新生成 Read View
,这时事务 101 已经提交,102 并未提交,所以此时 Read View
中活跃的事务 m_ids
:[102] ,m_low_limit_id
为:104,m_up_limit_id
为:102,m_creator_trx_id
为:103
- 此时最新记录的
DB_TRX_ID
为 102,m_up_limit_id <= 102 < m_low_limit_id,所以要在m_ids
列表中查找,发现DB_TRX_ID
存在列表中,那么这个记录不可见 - 根据
DB_ROLL_PTR
找到undo log
中的上一版本记录,上一条记录的DB_TRX_ID
为 101,满足 101 < m_up_limit_id,记录可见,所以在T6
时间点查询到数据为name = 李四
,与时间 T4 查询到的结果不一致,不可重复读!
3. 时间线来到 T9 ,数据的版本链为:

重新生成 Read View
, 这时事务 101 和 102 都已经提交,所以 m_ids 为空,则 m_up_limit_id = m_low_limit_id = 104,最新版本事务 ID 为 102,满足 102 < m_low_limit_id,可见,查询结果为 name = 赵六
总结: 在 RC 隔离级别下,事务在每次查询开始时都会生成并设置新的 Read View,所以导致不可重复读
在 RR 下 ReadView 生成情况
在可重复读级别下,只会在事务开始后第一次读取数据时生成一个 Read View(m_ids 列表)
1. 在 T4 情况下的版本链为:

在当前执行 select
语句时生成一个 Read View
,此时 m_ids
:[101,102] ,m_low_limit_id
为:104,m_up_limit_id
为:101,m_creator_trx_id
为:103
此时和 RC 级别下一样:
- 最新记录的
DB_TRX_ID
为 101,m_up_limit_id <= 101 < m_low_limit_id,所以要在m_ids
列表中查找,发现DB_TRX_ID
存在列表中,那么这个记录不可见 - 根据
DB_ROLL_PTR
找到undo log
中的上一版本记录,上一条记录的DB_TRX_ID
还是 101,不可见 - 继续找上一条
DB_TRX_ID
为 1,满足 1 < m_up_limit_id,可见,所以事务 103 查询到数据为name = 菜花
2. 时间点 T6 情况下:

在 RR 级别下只会生成一次Read View
,所以此时依然沿用 m_ids
:[101,102] ,m_low_limit_id
为:104,m_up_limit_id
为:101,m_creator_trx_id
为:103
- 最新记录的
DB_TRX_ID
为 102,m_up_limit_id <= 102 < m_low_limit_id,所以要在m_ids
列表中查找,发现DB_TRX_ID
存在列表中,那么这个记录不可见 - 根据
DB_ROLL_PTR
找到undo log
中的上一版本记录,上一条记录的DB_TRX_ID
为 101,不可见 - 继续根据
DB_ROLL_PTR
找到undo log
中的上一版本记录,上一条记录的DB_TRX_ID
还是 101,不可见 - 继续找上一条
DB_TRX_ID
为 1,满足 1 < m_up_limit_id,可见,所以事务 103 查询到数据为name = 菜花
3. 时间点 T9 情况下:

此时情况跟 T6 完全一样,由于已经生成了 Read View
,此时依然沿用 m_ids
:[101,102] ,所以查询结果依然是 name = 菜花
MVCC➕Next-key-Lock 防止幻读
InnoDB
存储引擎在 RR 级别下通过 MVCC
和 Next-key Lock
来解决幻读问题:
1、执行普通 select
,此时会以 MVCC
快照读的方式读取数据
在快照读的情况下,RR 隔离级别只会在事务开启后的第一次查询生成 Read View
,并使用至事务提交。所以在生成 Read View
之后其它事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下的 “幻读”
2、执行 select…for update/lock in share mode、insert、update、delete 等当前读
在当前读下,读取的都是最新的数据,如果其它事务有插入新的记录,并且刚好在当前事务查询范围内,就会产生幻读!InnoDB
使用 Next-key Lock 来防止这种情况。当执行当前读时,会锁定读取到的记录的同时,锁定它们的间隙,防止其它事务在查询范围内插入数据。只要我不让你插入,就不会发生幻读
0 条评论