undo日志
本文为阅读《MySql是怎样运行的》的笔记,供大家参考。
文章目录
概述
事务需要保证 原子性 ,如果事务过程种出现了异常,需要回滚数据,这时就用到了undo log
undo log 是回滚需要的,那么就需要记录 “增删改之前的数据” ,以保证回滚时,能回到增删改之前的状态。
一般来说,进行增删改操作时,对于一条记录会产生一条undo log,但是有些操作会产生两条:某些update操作会先删除再增加。
一个事务会产生很多条undo log,会被从0开始编号:0号、1号…n号undo log。这个编号被称作undo no。
undo log 是存放在 页
中的,类型为 FIL_PAGE_UNDO_LOG ,这些页面可以从系统表空间中分配,也可以从一种专门存放 undo日志 的表空间,也就是undo tablespace 中分配。
undo日志数据结构
insert 对应的undo log
对于insert的数据,回滚时将其删除就可以了,删除的条件当然就是根据主键,所以undo log就需要保存insert的数据的主键。
insert操作产生的undo log 类型为:TRX_UNDO_INSERT_REC
undo no 在一个事务
中是从 0 开始递增的,每生成一条 undo日志 ,该条日志的 undo no 就+1。
如果主键只包含一个列,那么只需要把该列占用的存储空间大小和真实值记录下来,如果含多个列,那么每个列占用的存储空间大小和对应的真实值都需要记录。
当insert一条记录时,实际上是在聚簇索引和二级索引中都插入了数据,但是在记录undo log,只需要考虑聚簇索引即可,因为在回滚删除聚簇索引时,也会同步删除掉二级索引中对应的记录,同样的,update, delete 也是如此。
delete 对应的undo log
在删除一条记录时,实际的步骤分为两步:
- 将删除标识置为1(还会修改事务id、回滚指针这样的隐藏列),这个操作被称为delete mark。
- 当该删除语句所在事务提交后,会有线程将其移动到垃圾链表中,将其真正的删除掉。这个操作被称为 purge 。
当然,删除过程中还会有系统记录的信息的改变,就比如页面头中记录的PAGE_GARBAGE属性,其记录了当前页面中可重用的部分大小,即垃圾链表的大小
同时,还有PAGE_FREE指向垃圾链表的头节点,当插入新的记录时,会判断头节点能否容纳新的记录,如果不可以则会申请一个新的空间来储存(不会依次遍历垃圾链表),而如果可以容纳,则直接重用此节点并将指针指向下一个垃圾节点。
如果垃圾头节点一直得不到利用,也不代表这部分空间被废弃了,而是当页面快满时,再插入一条记录,此时若剩余空间不足以容纳记录,则会判断垃圾链表的空间(PAGE_GARBAGE)和剩余空间一起是否能容纳,若可以,innodb就会重新组织页面内的记录:先开辟一个临时页面,把页面记录依次插入进去,则不会产生垃圾链表,之后再将临时页面的数据复制到本页面,这样就可以将这部分空间再次利用。
在提交事务之前,只做了delete mark 操作而已,undo log也只需要考虑事务未提交时的回滚问题。
delete对应的undo log 类型为 TRX_UNDO_DEL_MARK_REC
是的,deldet 对应的undo log中还需要记录旧的事务id和回滚指针: old trx_id 和 old roll_pointer。这样可以通过这条undo log 找到记录修改之前的undo log。
比如,在一个事务中对一条记录先加再删:
执行完delete mark 操作后,删除的undo log就和之前添加的undo log行成了一条链表。这个链表就称之为 版本链 。
相比于TRX_UNDO_INSERT_REC ,TRX_UNDO_DEL_MARK_REC 还多了一个索引列各列信息
,是指当记录的某一列被包含在某索引中,则就需要被记录该列在记录中的位置(pos)、长度(len)、具体值(value)。
一条记录列的排列顺序为:row_id(主键)、trx_id、roll_pointer、用户记录依次排列。主键一般pos 为0。
update 对应的undo log
对于update要分两种情况讨论:更新主键的操作和不更新主键的操作。
不更新主键的操作
不更新主键的操作又可以分为,新的记录各列数据大小与旧记录相同,和不同的情况。此两种不同的情况对应了不同的更新操作:直接更新;先删除再添加
-
直接更新(就地更新,in-place update )
更新记录时,对于被更新的每个列来说,如果更新后的列和更新前的列占用的存储空间都一样大,那么就可以进行就地更新 ,也就是直接在原记录的基础上修改对应列的值。
-
先删除再添加
如果有任何一个被更新的列更新前和更新后占用的存储空间大小不一致,那么就需要先把这条旧的记录从聚簇索引页面中删除掉,然后再根据更新后列的值创建一条新的记录插入到页面中。
此处不是只进行delete mark,而是执行了完整的删除操作,即:将删除标识置为1,移入垃圾链表,并修改相应的系统信息( PAGE_FREE 、PAGE_GARBAGE等) 。(因为主键没有变,直接删除掉旧的,再添加其他事务也可以根据主键找到这条记录)
不过进行purge操作的不再是删除专用的线程,而是由用户线程同步执行的 。
不更新主键的操作对应的undo类型为 TRX_UNDO_UPD_EXIST_REC
n_updated 属性表示本条 UPDATE 语句执行后将有几个列被更新,后边跟着的 <pos, old_len, old_value>分别表示被更新列在记录中的位置、更新前该列占用的存储空间大小、更新前该列的真实值。
如果在 UPDATE 语句中更新的列包含索引列,那么也会添加 索引列各列信息 这个部分,否则的话是不会添加这个部分的。
更新主键的操作
在聚簇索引中,页面中的数据是按照主键排序以单链表的形式存在的,这就意味着,如果修改了主键,那么这条记录的储存位置会发生改变。
因此,在更新主键的update操作分为两步:先delete mark,再添加
-
原记录delete mark
注意,此处是delete mark,并没有将原记录加入垃圾链表。这么做的目的是为了MVVC服务的,不然会导致其他事务根据主键查询不到这条记录,因为位置改变了。
-
添加修改后的记录
针对 UPDATE 语句更新记录主键值的这种情况,在对该记录进行 delete mark 操作前,会记录一条类型为TRX_UNDO_DEL_MARK_REC 的 undo日志 ;之后插入新记录时,会记录一条类型为 TRX_UNDO_INSERT_REC 的 undo日志 ,也就是说每对一条记录的主键值做改动时,会记录2条 undo日志 。
undo log 页面
通用结构
表空间是由许多页面组成的,这些页面又有不同的类型。而undo log就在页面中进行存储的,存储undo log 的页面类型为: FIL_PAGE_UNDO_LOG
-
TRX_UNDO_PAGE_TYPE :本页面准备存储什么种类的 undo日志
好几种类型的undo log,其实可以分为两大类,也就是此属性的两个可选值,不同大类的 undo日志 不能混着存储 :
TRX_UNDO_INSERT(1表示):TRX_UNDO_INSERT_REC 属于此类,一般由 INSERT 语句产生,或者在 UPDATE 语句中有更新主键的情况也会产生
TRX_UNDO_UPDATE(2表示):除了 TRX_UNDO_INSERT_REC 之外的其他类型都属于此类,一般由 DELETE 、 UPDATE 语句产生
分成两个大类,是因为TRX_UNDO_INSERT_REC在事务提交后可以直接删除掉,而其他类型还需要为MVCC服务,不能直接删除掉
-
TRX_UNDO_PAGE_START :第一条 undo log在本页面中的起始偏移量,即表示是从什么位置开始存储 undo log的
-
TRX_UNDO_PAGE_FREE :与上边的 TRX_UNDO_PAGE_START 对应,表示当前页面中存储的最后一条 undo 日志结束时的偏移量,即从这个位置开始,可以继续写入新的 undo日志 。
-
TRX_UNDO_PAGE_NODE :代表一个 List Node 结构 ,因为undo页面也是以链表形式连接的
undo log 页面链表
链表中的第一个 Undo页面称为 first undo page ,其余的 Undo页面称为 normal undo page 。相较于normal page,first page中除了记录 Undo Page Header 之外,还会记录其他的一些管理信息。(在通用结构基础上多了一些系统信息,此外才是记录的undo log)
undo日志有不同的类型,分为两个大类,undo页面要么只存储 TRX_UNDO_INSERT 大类的 undo日志 ,要么只存储TRX_UNDO_UPDATE 大类的 undo日志(页面头中的TRX_UNDO_PAGE_TYPE 属性)。
链表也是如此,一条链表只能有一类undo页面,一个称之为 insert undo链表 ,另一个称之为 update undo链表。
innodb规定,普通表和临时表的记录改动时产生的 undo日志 要分别记录
因此,一个事务中最多有4个以 Undo页面 为节点组成的链表:
此外,并不是在事务一开始就会为这个事务分配这4个链表,刚刚开启事务时,一个 Undo页面 链表也不分配,而是需要的时候才分配
比如:当对普通表进行insert操作或者执行更新记录主键的操作时,产生了TRX_UNDO_INSERT大类的undo log,才会分配 储存普通表undo log 的 insert undo链表
。
对于多个事务的情况,不同事务执行过程中产生的undo日志会被写入到不同的undo页面链表中。
假设有两个不同的事务分别执行sql语句,那么可能会产生一下的undo log 链表:
first undo page
对于链表第一张页面,会有一些其他的系统信息:
Undo Log Segment Header
每一个 Undo页面 链表都对应着一个 段 ,称之为 Undo Log Segment 。也就是说链表中的页面都是从这个段里边申请的。
first undo page 中的Undo Log Segment Header就包含了该链表对应的段的 segment header 信息以及其他的一些关于这个段的信息
简单讲,这个 段 是一个逻辑上的概念,本质上是由若干个零散页面和若干个完整的区组成的。
B+ 树索引被划分成两个段,一个叶子节点段,一个非叶子节点段,这样叶子节点就可以被尽可能的存到一起,非叶子节点被尽可能的存到一起。
其中各个属性的意思如下:
-
TRX_UNDO_STATE :本 Undo页面 链表处在什么状态。
一个 Undo Log Segment 可能处在的状态包括:- TRX_UNDO_ACTIVE :活跃状态,也就是一个活跃的事务正在往这个段里边写入 undo日志 。
- TRX_UNDO_CACHED :被缓存的状态。处在该状态的 Undo页面 链表等待着之后被其他事务重用。
- TRX_UNDO_TO_FREE :对于 insert undo 链表来说,如果在它对应的事务提交之后,该链表不能被重用,那么就会处于这种状态。
- TRX_UNDO_TO_PURGE :对于 update undo 链表来说,如果在它对应的事务提交之后,该链表不能被重用,那么就会处于这种状态。
- TRX_UNDO_PREPARED :包含处于 PREPARE 阶段的事务产生的 undo日志 。
-
TRX_UNDO_LAST_LOG :本 Undo页面 链表中最后一个 Undo Log Header 的位置。
-
TRX_UNDO_FSEG_HEADER :本 Undo页面 链表对应的段的 Segment Header 信息。
-
TRX_UNDO_PAGE_LIST : Undo页面 链表的基节点。
Undo页面 的 Undo Page Header 部分有一个TRX_UNDO_PAGE_NODE 属性,代表了一个 List Node 结构。Undo页面可以通过这个属性连成一个链表。这个 TRX_UNDO_PAGE_LIST 属性代表着这个链表的基节点,当然这个基节点只存在于 first undo page中。
Undo Log Header
同一个事务向一个 Undo页面 链表中写入的 undo日志 算是一个组,也就是每个undo链表中的log就算做一个组。
比如,某事务分配了:普通表insert链表、普通表update链表、临时表insert链表 3个Undo页面链表,也就会写入3个组的 undo日志。
在每写入一组日志时,都会在链表的第一个页面记录一下这个组的一些属性,即使用first undo page 的 Undo Log Header 记录。
因此,Undo页面链表的第一个页面在真正写入undo日志前,都会被填充 UndoPage Header 、 Undo Log Segment Header 、 Undo Log Header 这3个部分
-
TRX_UNDO_TRX_ID :生成本组 undo日志 的事务 id 。
-
TRX_UNDO_TRX_NO :事务提交后生成的一个需要序号,使用此序号来标记事务的提交顺序(先提交的此序号小,后提交的此序号大)。
-
TRX_UNDO_DEL_MARKS :标记本组 undo 日志中是否包含由于 Delete mark 操作产生的 undo日志 。
-
TRX_UNDO_LOG_START :表示本组 undo 日志中第一条 undo日志 的在页面中的偏移量。
-
TRX_UNDO_XID_EXISTS :本组 undo日志 是否包含XID信息。
-
TRX_UNDO_DICT_TRANS :标记本组 undo日志 是不是由DDL语句产生的。
-
TRX_UNDO_TABLE_ID :如果 TRX_UNDO_DICT_TRANS 为真,那么本属性表示DDL语句操作的表的 table id 。
-
TRX_UNDO_NEXT_LOG :下一组的 undo日志 在页面中开始的偏移量。
-
TRX_UNDO_PREV_LOG :上一组的 undo日志 在页面中开始的偏移量。
一般来说,一个undo链表只会储存一个事务产生的undo log,但在某些情况下,一些undo链表会被重新利用,并且之前事务产生的undo log不会被覆盖
因此,一个undo页面可能会储存多组的undo log。TRX_UNDO_NEXT_LOG和TRX_UNDO_PREV_LOG就是用来标记下一组和上一组undo日志在页面中的偏移量的。
-
TRX_UNDO_HISTORY_NODE :一个12字节的 List Node 结构,代表一个称之为 History 链表的节点。
因此undo链表就是如下图所示的样子:
undo页面重用
为了能提高并发执行的多个事务写入 undo日志 的性能 ,数据库会为一个事务分配最多四条undo链表,但是可能大部分事务执行的时候都是只改了一条或几条数据,记录的undo log数量非常有限,因此每个事务都开辟一个或以上的undo链表太浪费空间了。所以有些undo链表就可以被重新利用起来:
-
该链表中只包含一个 Undo页面,且该undo页面已经使用的空间小于整个页面的3/4。
如果一个事务产生了较多的undo log,那么undo 链表就比较长,提交之后一个新的事务重用其undo 链表的话,新的事务又只产生了较少的日志,就会导致还需要维护较长的链表,得不偿失。
对于 insert和update链表,又会有不同的策略:
-
insert undo链表
insert undo链表 中只存储类型为 TRX_UNDO_INSERT_REC 的 undo日志 ,(不需要为MVCC服务),所以在事务提交之后可以直接被覆盖掉。
-
undate undo 链表
在一个事务提交后,它的 update undo链表 中的 undo日志不能立即删除掉,如果想要在事务提交后重用update链表,就相当于在同一个 Undo页面 中写入了多组的 undo日志。
first undo page页面的Undo log header中的TRX_UNDO_NEXT_LOG 、TRX_UNDO_PREV_LOG 部分就是为此而存在的。
并且,写入新的一组undo log之前也会在旧的undo log 之后写入新的 undo log header。
回滚段
概述
在多个事务并发的情况下,系统中产生的undo链表是很多的,因此innodb为了方便管理,又定义了一个 Rollback Segment 的概念,也就是回滚段。
在聚簇索引中,叶子节点页面为一个段,非叶子节点页面为一个段,但是回滚段中只有一个页面 Rollback Segment Header
这个页面中存放了各个undo链表的 frist undo page 的页号 ,称之为 undo slot 。一个回滚段头页面中有1024个undo slot。
在innodb1.1(mysql5.5)之前,一个系统内有一个回滚段,也就是说最多支持1024个undo链表的事务并发执行。
而在innodb1.1(mysql5.5)之后,数量提升到了128个,也就是最多支持128*1024
-
TRX_RSEG_MAX_SIZE :本回滚段管理的所有Undo链表中Undo页面数量之和的最大值。
该属性的值默认0xFFFFFFFE;而四个字节表示的最大值为0xFFFFFFFF,这个值有特殊用途
-
TRX_RSEG_HISTORY_SIZE : History 链表占用的页面数量。
-
TRX_RSEG_HISTORY : History 链表的基节点。
-
TRX_RSEG_FSEG_HEADER :本 Rollback Segment 对应的10字节大小的 Segment Header 结构,通过它可以找到本段对应的 INODE Entry 。
-
TRX_RSEG_UNDO_SLOTS :undo slot 集合。一个页号占用 4 个字节,共需 1024 × 4 = 4096 个字节
从回滚段中申请Undo页面链表
初始状态下,回滚段没有被分配任何的undo链表,其undo slot值被设置为了:FIL_NULL(对应的十六进制就是 0xFFFFFFFF )
当有事务开始需要分配undo链表了,就从第一个undo slot开始遍历是否是FIL_NULL,若是,则在表空间内新建一个 undo log段(一个undo链表对应一个段),然后从段内申请一个first undo page,然后将undo slot的值设置为该页面的地址。
当事务提交时:
-
如果它的undo链表符合被重用的条件:
Undo链表的 TRX_UNDO_STATE 属性会置为TRX_UNDO_CACHED。
其占用的slot也会处于缓存状态,被缓存的 undo slot 都会被加入到一个链表,根据对应的 Undo页面 链表的类型不同,也会被加入到不同的链表:
- 如果是 insert undo链表 ,则该 undo slot 会被加入 insert undo cached链表 。
- 如果是 update undo链表 ,则该 undo slot 会被加入 update undo cached链表 。
一个回滚段就对应着这两个cached链表 ,如果有新事务要分配 undo slot 时,先从对应的 cached链表 中找。如果没有,才会到回滚段的 Rollback Segment Header页面中再找
-
如果它的undo链表不符合被重用的条件:
-
如果是 insert undo链表 ,则该undo链表的TRX_UNDO_STATE 属性会被设置为 TRX_UNDO_TO_FREE ,之后该链表对应的段会被释放掉,然后把该 undo slot 的值设置为 FIL_NULL 。
-
如果是 update undo链表 ,则 undo链表的 TRX_UNDO_STATE 属性会被设置为 TRX_UNDO_TO_PRUGE ,同时将该 undo slot 的值设置为 FIL_NULL ,然后将本次事务写入的一组undo 日志放到 History链表中(并不会将Undo链表对应的段释放掉,这些 undo 日志还有用)。
-
多个回滚段
在innodb1.1(mysql5.5)之后,系统中的回滚段数量提升到了128个,也就是最多支持128*1024个undo链表的读写事务并发执行。
在系统表空间的第 5 号页面包含了128个8字节大小 区域用于存放回滚段Rollback Segment Header 页面的地址 。
不同的回滚段可能分布在不同的表空间中。
因此,每个8字节由两部分组成:
- 4字节大小的 Space ID ,代表表空间的ID
- 4字节大小的 Page number ,代表页号
总结:在系统表空间的第 5 号页面中存储了128个 Rollback Segment Header页面地址,每个 Rollback Segment Header 就相当于一个回滚段。在 Rollback Segment Header 页面中,又包含 1024 个 undo slot ,每个 undo slot 都对应一个 Undo页面 链表。
这128个回滚段可以被分成两大类:
- 第 0 号、第 33~127 号回滚段属于一类。其中第 0 号回滚段必须在系统表空间中,第 33~127 号回滚段既可以在系统表空间中,也可以在自己配置的 undo 表空间中。
- 第 1~32 号回滚段属于一类。这些回滚段必须在临时表空间中,临时表的 Undo页面 链表时,必须从这一类的段中分配相应的 undo slot 。
之所以需要分为临时表和普通版,是因为undo页面也是页面的一种,只要是页面做了修改,都需要产生redo日志,而对于临时表undo日志是不需要进行崩溃恢复的,因为修改临时表而产生的 undo日志 只需要在系统运行过程中有效 。
因此在修改针对普通表的回滚段中的Undo页面时,需要记录对应的redo日志,而修改针对临时表的回滚段中的Undo页面时,不需要记录对应的redo日志。
undo log的写入过程
首先会到系统表空间的第 5 号页面中分配一个回滚段(获取一个 Rollback Segment Header 页面的地址),一个事务只对应一个回滚段,分配方式为循环分配:即上一个分配的0号回滚段,则下一个分配33号回滚段(0、33-128为普通表回滚段)。
然后检查此回滚段的 cached链表,寻找是否有缓存起来的undo slot(其对应的undo链表可重用),若有则直接分配,若没有则找一个新的,也是循环分配。
找到可用的undo slot后,若为缓存的,则说明是重用undo 链表,已经分配了 Undo LogSegment(包含一个undo链表),若不是缓存的,则需要重新分配一个 Undo LogSegment然后申请一个页面作为undo链表头,即first undo page。
然后事务就可以把 undo日志 写入到上边申请的 Undo页面 链表了
在向Undo页面 中写入 undo日志时的方式是十分简单暴力的,就是写完一条紧接着写另一条,各条 undo日志 之间是亲密无间的。写完一个 Undo页面后,再从段里申请一个新页面,然后把这个页面插入到 Undo页面 链表中,继续往这个新申请的页面中写。