总结自
bojiangzhou
undo log
称为撤销日志或回滚日志。在一个事务中进行增删改操作时,都会记录对应的 undo log。在对数据库进行修改前,会先记录对应的 undo log,然后在事务失败或回滚的时候,就可以用这些 undo log 来将数据回滚到修改之前的样子。
InnoDB 在内存维护了一个全局变量来表示事务ID,每当要分配一个事务ID时,就获取这个变量值,然后把这个变量自增1
。
在行记录格式中行记录格式,行记录中会有三个隐藏列:
-
DB_ROW_ID
:如果没有为表显式的定义主键,并且表中也没有定义唯一索引,那么InnoDB会自动为表添加一个row_id
的隐藏列作为主键。 -
DB_TRX_ID
:事务中对某条记录做增删改时,就会将这个事务的事务ID写入trx_id
中。 -
DB_ROLL_PTR
:回滚指针,本质上就是指向 undo log 的指针。
Undo Log Type
每对一条记录做一次改动,就会产生1
条或者2
条 undo log
。一个事务中可能会有多个增删改SQL语句,这些 undo log 会被从 0
开始递增编号,这个编号称为 undo no
。
insert
插入一条数据对应的undo
操作其实就是根据主键删除这条数据就行了。所以 insert 对应的 undo log 主要是把这条记录的主键记录上。
比如我们开启了一个事务,向 account 中插入两条数据:
BEGIN;
INSERT INTO account(id,card,balance) VALUES (1, 'AA', 0),(2, 'BB', 0);
假设这个事务的事务ID为100
,这条INSERT语句会插入两条数据,就会产生两个 undo log。插入记录的时候,会在行记录的隐藏列事务ID中写入当前事务ID,并产生 undo log,记录中的回滚指针会保存 undo log 的地址。而同一个页中的多条记录会通过next_record
连接起来形成一个单链表,这块可以参考前面的行记录格式和数据页结构相关的文章。
delete
删除一条数据大致可以分为两个阶段:
-
阶段一
首先是用户线程执行删除时,会先将记录头信息中的 delete_mask
标记为 1
,而不是直接从页中删除,因为可能其它并发的事务还需要读取这条数据。(后面讲MVCC的时候就知道为什么了)
-
阶段二
提交事务后,后台有一个 purge
线程会将数据真正删除。
首先要知道,页中的数据是通过记录头信息中的 netx_record
连接起来的单向链表(假设这个链表称为数据链表
)。页中还有另一个链表,称为垃圾链表
,记录真正删除后,会从数据链表中移除,然后加入到垃圾链表的头部,以便重用空间。
所以阶段二就是将记录从数据链表移除,加入到垃圾链表的头部。
也就是说,删除操作在事务提交前,只会经历阶段一,就是将记录的 delete_mask
标记为 1
。
此时接着执行一条删除的SQL语句,将id=2的这条数据删除:
BEGIN;
INSERT INTO account(id,card,balance) VALUES (1, 'AA', 0),(2, 'BB', 0);
DELETE FROM account WHERE id = 2;
因为是在同一个事务中,所以记录中的隐藏列trx_id
没变,记录头中的delete_mask
则标记为1
了。然后生成了一个新的 undo log,并保存了记录中原本的trx_id
和roll_pointer
,所以这个新的 undo log 就指向了旧的 undo log,而记录中的 roll_pointer 则指向这个新的 undo log。注意 undo log 中的事务编号也在递增。
update
在更新一条记录时,要把被更新的列的旧值记下来,这样之后回滚时再把这些列更新为旧值就好了。
Undo Log存储
undo log 分类
前边介绍了几种类型的 undo log,它们其实被分为两个大类来存储:
-
TRX_UNDO_INSERT
类型为 TRX_UNDO_INSERT_REC 的 undo log 属于此大类,一般由 INSERT 语句产生,或者在 UPDATE 更新主键的时候也会产生。
-
TRX_UNDO_UPDATE
除了类型为 TRX_UNDO_INSERT_REC 的 undo log,其他类型的 undo log 都属于这个大类,比如 TRX_UNDO_DEL_MARK_REC 、 TRX_UNDO_UPD_EXIST_REC ,一般由 DELETE、UPDATE 语句产生。
之所以要分成两个大类,是因为不同大类的 undo log 不能混着存储,因为类型为TRX_UNDO_INSERT_REC
的 undo log 在事务提交后可以直接删除掉,而其他类型的 undo log 还需要提供MVCC
功能,不能直接删除。
undo 页面链表
undo log
是存放在FIL_PAGE_UNDO_LOG
类型的页中,一个事务中可能会产生很多 undo log,也许就需要申请多个undo页,所以 InnoDB 将其设计为一个链表的结构,将一个事务
中的多个undo页连接起来。
但是前面说了 undo log 分为两大类,不能混着存储,所以如果事务中产生了这两大类型的 undo log,会创建两个链表,一个用来存储 TRX_UNDO_INSERT
类别的 undo log,一个用来存储 TRX_UNDO_UPDATE
类别的 undo log。
如果事务中还修改了临时表,InnoDB规定对普通表和临时表修改产生的 undo log 要分开存储,所以在一个事务中最多可能会有4
个 undo 页面链表。
需要注意的是这些链表并不是事务一开始就分配好的,而是在需要某个类型的链表的时候才会去分配。
回滚段
redo log
是存放在重做日志文件中的,而 undo log
默认是存放在系统表空间中的一个特殊段(segment)
中,这个段称为回滚段(Rollback Segment
),链表中的页面都是从这个回滚段里边申请的。
InnoDB定义了128
个回滚段(Rollback Segment
),也就有128
个 Rollback Segment Header
,每个Rollback Segment Header
页面都对应着一个回滚段。一个 Rollback Segment Header 页面中包含1024
个undo slot
,每个 undo slot 存放了 undo 链表头部的 undo 页的页号。就有128*1024=131072
个undo slot
,也就是说最多同时支持131072
个并发事务执行。
在系统表空间的第5
号页面中存储了这128
个Rollback Segment Header
页面地址。
事务回滚
前面在一个事务中增删改产生的一系列 undo log,都有 undo no
编号的。在回滚的时候,就可以应用这个事务中的 undo log,根据 undo no
从大到小开始进行撤销操作,就将数据还原为原来的样子了。
但需要注意的是,undo log 是逻辑日志
,只是将数据库逻辑地恢复
到原来的样子。所有修改都被逻辑地取消了,但是数据结构和页本身在回滚之后可能大不相同。因为同时可能很多并发事务在对数据库进行修改,因此不能将一个页回滚到事务开始的样子,因为这样会影响其他事务正在进行的工作。
MVCC
另一篇博客