MySQL的日志--Undo Log【学习笔记】
MySQL的日志--Undo Log
知识来源:
- 《MySQL是怎样运行的》--- 小孩子4919
为了保证事务的原子性,当事务中途遇到各种错误需要将数据回滚(rollback)到原来的样子。为此MySQL提出撤销日志(Undo Log,也称undo日志)。
事务id
-
InnoDB的每条行记录都有一个隐藏列trx_id,这个便是记录修改当前记录的事务的id。
-
事务的id存储在系统表空间页号为5的页中一个名为Max Trx ID的属性中
-
每次MySQL启动时会将这个值读到内存中,并且加256
-
每次分配给事务后,自增1
-
等到这个值为256的倍数时,就会将这个值持久化到Max Trx ID
-
-
事务id只有在事务中有语句对用户表、临时表进行增删改操作后才会被分配给事务,否则该事务的id为0。
-
start transaction read only,开启一个只读事务,对临时表增删改后才会被分配事务id。
-
start transaction [read write],开启一个读写事务,对用户表、临时表增删改后才会被分配事务id。
-
undo日志的格式
-
每条undo日志都会有一个undo no,在一个事务中从0开始递增,每个事务都会在内部维护一个undo no
-
Undo Log会存在于FIL_PAGE_UNDO_LOG类型(0x0002)的页中,这些页存在于系统表空间或者undo表空间(undo tablespace)。
-
undo日志被分为两个大类TRX_UNDO_INSERT(插入类型)和TRX_UNDO_UPDATE(修改类型)
-
大类下面分为需要小类,用undo type的字段表示。
-
-
聚簇索引(一棵b+树)中的叶子节点存放着一条条记录, 每条记录都有一个隐藏字段roll_pointer,指向undo日志的起始地址,roll_pointer的结构如下:
-
-
-
is_insert:是否为TRX_UNDO_INSERT大类的undo日志
-
resg id:undo日志所在回滚段的编号。回滚段编号为0~127。
-
page number:undo日志所在的页号。
-
offset:undo日志在页面中的偏移量。
-
TRX_UNDO_INSERT大类的undo日志
INSERT操作对应的undo日志
-
undo type为TRX_UNDO_INSERT_REC
-
会记录主键各字段的长度(len)和值(value),用于未来在聚簇索引中删除这条插入的记录,还可以根据主键删除对应的二级索引记录
TRX_UNDO_UPDATE大类的undo日志
-
这个大类的undo日志中都会包含trx_id和roll_pointer,用于形成版本链
-
每次对一条记录生成undo日志时,就会将该记录的trx_id和roll_pointer,赋值给这条新的undo日志。
-
DELETE操作对应的undo日志
在介绍删除操作的undo日志之前,首先得说明一下记录的删除各阶段。
delete mark阶段:将记录的delete_flag设置为1,意味逻辑删除。记录还是在原来位置没有动。
purge阶段:等待删除语句所在的事务提交后,就会有专门的后台线程将该记录转移到垃圾链表(PAGE_FREE链表)的头部。并且将页面的其他管理信息修改,如:PAGE_LAST_INSERT、PAGE_GARBAGE会加上移入垃圾链表的记录的大小。
等到后续插入的时候,会优先判断FREE_PAGE的头节点空间是否足够,可以的话就直接重用被删除记录的空间,当然可能会产生碎片(新纪录小于被删除记录)。如果不行,就从页面最后一条记录后面开辟出一块空间。
如果连后面的空间都没有,则会统计剩余空间和碎片总体空间大小,如果这两者大于新插入的记录,则会重整页面(开辟一个新页面,将原来页面的记录顺序复制),插入新纪录。
-
undo type为TRX_UNDO_DEL_MARK_REC
-
相较于TRX_UNDO_INSERT大类的undo日志,除了主键各字段的信息,还存储着各个索引列各字段的信息,该列在记录中的位置(pos)、长度(len)和值(value)
-
这些信息主要用于purge阶段真正删除记录。TODO
-
UPDATE操作对应的undo日志
更新语句产生的undo日志被分为四种情况。
-
不更新索引列
-
原地更新(in-place update)
-
当更新前后每个列占用空间大小没有变化,则会进行就地更新。仅生成一条undo日志。
-
-
删除旧纪录、插入新纪录
-
不满足就地更新的条件时,用户线程(不是后台线程)会将原来的记录删除(直接扔进PAGE_FREE链表)。
-
在插入新纪录后,就会生成一条TRX_UNDO_UPD_EXIST_REC类型的undo日志。
-
-
相较于TRX_UNDO_DEL_MARK_REC类型的undo日志最大的区别,就是多了被更新列的信息,列在记录中的位置(pos)、长度(old_len)和值(old_value)
-
-
-
-
更新主键列
-
逻辑删除原纪录(delete mark操作)。事务提交后,才会真正删除。此时会生成一条TRX_UNDO_DEL_MARK_REC类型的undo日志
-
根据更新各个字段,形成一条新纪录插入,然后生成一条TRX_UNDO_INSERT_REC类型的undo日志
-
TODO,如何回滚
-
-
-
更新二级索引列
-
和更新主键类似
-
唯一不同的点,就是更新主键列,会将事务id赋值给那条新插入的记录的trx_id。
-
而二级索引插入的新纪录没有这个隐藏列,所以就把事务的id赋值给页面中Page Header中的PAGE_MAX_TRX_ID字段,表示该页面中修改记录的最大事务id。
-
-
FIL_PAGE_UNDO_LOG类型页的结构
这种类型的页面简称undo页。undo页面会通过链表结构互相串起来,而链表的头节点存放在第一个undo页,被称为first undo page,其余的页均被称为normal undo page。first undo page比normal undo page多了两部分信息。
每个事务根据undo日志类型的不同(插入、更新)、操作表的类型不同(用户表、系统表),而分配到四种不同类型undo页的链表。
first undo page
-
File Header和File Trailer是innodb页的基本结构,每种类型的页面都有这两个结构。
-
Undo Page Header
-
TRX_UNDO_PAGE_TYPE:表示该undo页存储的undo日志大类
-
前面提到过undo日志被分为TRX_UNDO_INSERT和TRX_UNDO_UPDATE两个大类。存放TRX_UNDO_INSERT类型undo记录的undo页不能存储TRX_UNDO_UPDATE类型的undo记录。
-
因为TRX_UNDO_UPDATE类型的undo记录在事务提交后,还需要为MVCC服务。而TRX_UNDO_INSERT类型的undo日志在事务提交后,就可以被回收了。
-
-
TRX_UNDO_PAGE_START:表示页面第一条undo日志开始时的偏移量。
-
TRX_UNDO_PAGE_FREE:表示页面中最后一条undo日志结束时的偏移量。
-
-
TRX_UNDO_PAGE_NODE:表示双向链表结构节点。
-
-
-
Undo Log Segment Header:每个undo页链表对应着一个undo日志段。在事务刚开始生成undo日志之前,会先分配一个undo日志段,从这个段中分配到的第一个页面就是first undo page,而Undo Log Segment Header就存储这个段的一些信息。
-
-
TRX_UNDO_STATE:本undo页面链表的状态。
-
TRX_UNDO_ACTIVE:一个活跃的事务正在向这个undo页链表中写入undo日志
-
TRX_UNDO_CACHED:处于该状态的undo页链表等待之后被其他事务重用
-
TRX_UNDO_FREE:对于insert undo链表来说,如果在它对应的事务提交之后,该链表不能被重用,那么就会处于这种状态
-
TRX_UNDO_PURGE:对于update undo链表来说,如果在它对应的事务提交之后,该链表不能被重用,那么就会处于这种状态
-
TRX_UNDO_PREPARED:处于此状态的undo页链表用于存储处于PREPARED阶段的事务产生的日志。
-
-
TRX_UNDO_LAST_LOG:本undo页面链表中最后一个Undo Log Header的位置。
-
TRX_UNDO_FSEG_HEADER:这个就是指向那个段的指针
-
TRX_UNDO_PAGE_LIST:表示undo页链表的头节点
-
-
-
Undo Log Header
-
-
TRX_UNDO_TRX_ID:生成本组undo日志的事务id
-
TRX_UNDO_TRX_NO:事务提交后生成的一个序号,先提交的序号小,后提交的序号大
-
TRX_UNDO_NEXT_LOG:下一组undo日志在页面中开始的偏移量
-
TRX_UNDO_PREV_LOG:上一组undo日志在页面中开始的偏移量
-
一个事务在undo链表中插入的undo日志算一组,按理说应该只有一组,因为undo链表同一时间只能被一个事务占有。但是undo链表可以被重用,如果被重用的话,那么就会有多组undo日志存在于一个undo页链表了。
-
-
-
undo日志
-
每条undo日志都是紧密相连,十分紧凑。
-
normal undo page
-
相较于first undo page,少了两大块信息
回滚段
每个事务在执行过程中,至多被分配四个类型的undo页链表。这些undo链表的first undo page的页号则被存放在回滚段中的第一个页面的TRX_RSEG_UNDO_SLOTS中,当然整个回滚段中就只有这么一个页面,叫做Rollback Segment Header。
-
TRX_RSEG_MAX_SIZE:这个回滚段中管理的所有undo页链表中的undo页数量之和的最大值,默认值为0xFFFFFFFE
-
TRX_RSEG_HISTORY_SIZE:History链表占用的页面数量。
-
TRX_RSEG_HISTORY:History链表的头节点。
-
TRX_RSEG_FSEG_HEADER:指向回滚段。
-
TRX_RSEG_UNDO_SLOTS:每个undo页链表的first undo page的页号,每一个页号都叫做一个undo slot,一共有1024个。如果该undo slot没有指向undo页,则值为FIL_NULL(0xFFFFFFFF)
重用undo链表
-
需要满足
-
链表中只有一个undo页
-
该undo页使用空间小于整个页面空间的3/4
-
-
根据undo链表类型的不同而放入不同的cache链表,并且first undo page的Undo Log Segment Header的TRX_UNDO_STATE被设置为TRX_UNDO_CACHED
-
insert undo链表:undo slot被放入insert into cached链表
-
update undo链表:undo slot被放入update into cached链表
-
-
如果可以被重用,那么新事务则会重用undo页链表
-
insert undo链表:直接从头开始覆盖
-
-
update undo链表:从末尾追加。
-
-
-
如果不能被重用
-
insert undo链表:first undo page的Undo Log Segment Header的TRX_UNDO_STATE被设置为TRX_UNDO_FREE。之后Undo Log Segment会被释放掉,然后undo slot置为FIL_NULL
-
update undo链表:first undo page的Undo Log Segment Header的TRX_UNDO_STATE被设置为TRX_UNDO_PURGE。然后undo slot置为FIL_NULL。Undo Log Segment用于MVCC,当最后一个引用该段的事务也提交了,这个段才会被释放掉。
-
从回滚段中申请undo页面的流程如下:
-
首先查看两个cached链表中是否有可重用的undo slot,如果有则直接分配
-
没有的话,就从头开始遍历undo slot,如果值为FIL_NULL,则新分配一个段,创建first undo page,将该页号赋值给undo slot
-
否则,查找下一个undo slot,全部undo slot都被分配了,就会报错"Too many active concurrent transactions"
系统表空间页号为5的页面
如果说系统中只有一个回滚段,那么最多分配出1024个undo页的链表,假设一个事务分配一个undo页链表,那也就支持1024个事务的并发,那颗太少了。所以系统表空间中存在一个页号为5的页面,里面存放着128个回滚段的地址,也就是说系统可以最多分配1024*128个undo页链表,那很够了。
-
表空间id+页号,指向一个回滚段。
-
回滚段编号分为0~127
-
0号回滚段必须存在于系统表空间中
-
1~32号回滚段必须存在于临时表空间中(数据目录中的ibtmp1文件)
-
33~127号回滚段默认存在于系统表空间中,也可以自己配置的undo表空间(undo tablespace)
-
-
为什么要将回滚段分成两大类呢?
-
因为修改页面会记录redo日志保证持久化,那么保证undo页的持久化也需要redo日志。记录redo日志就需要消耗性能,对于临时表来说,系统崩溃后恢复根本不需要恢复,因为本来表就是临时的,那么针对这个回滚段的undo页就不会生成对应的redo日志。
-
-
现在讲讲事务分配undo页链表的全过程。
-
首先遍历第五号页面的128个回滚段指针,如果是针对临时表的事务就分配1~32号回滚段,否则分配0或33~127号回滚段。每个回滚段只能分配给一个事务,当前事务分配了0号回滚段,那下一个事务就只能分配到33号回滚段。
-
通过回滚段指针,找到对应的回滚段,首先查找对应的cached链表是否有可重用的undo slot,如果有,则直接重用对应的Undo Log Segment(当然undo页的类型需要对上,TRX_UNDO_UPDATE类型的undo链表只能被分配给需要TRX_UNDO_UPDATE类型undo链表的事务)
-
如果cached链表为空,此时就需要遍历undo slot,找到值为FIL_NULL的undo slot,并且申请一个Undo Log Segment,并申请first undo page,之后将该页的页号填写到undo slot中
-
之后就可以写入undo日志了。
-
回滚段的相关配置
-
innodb_rollback_segments:回滚段的数量
-
设置为1,那么系统会分配0~32号回滚段,1~32号回滚段针对临时表
-
设置为1~33,系统依然分配0~32号回滚段,1~32号回滚段针对临时表
-
设置为33~128,系统会分配对应设置数目的回滚段,1~32号回滚段针对临时表,其余针对普通表
-
-
innodb_undo_directory:设置undo表空间所在的目录,默认为数据目录
-
innodb_undo_tablespaces:undo表空间的数量,33~127号回滚段会平均分配到不同undo表空间中。
-
undo表空间体积过大,会自动阶段成小文件,而系统表空间只能不断增大,直到文件系统出错。
-
系统初始化配置了undo表空间,那么0号回滚段就不可用。
-
系统恢复回滚未完成的事务
-
首先加载系统表空间页号为5的页面
-
查看0和33~127号回滚段,遍历每个值不为FIL_NULL的undo slot
-
找到对应的undo页链表的first undo page,筛选出Undo Segment Header中TRX_UNDO_STATE为TRX_UNDO_ACTIVE类型的,表示该undo链表生前在内存中,正在被一个事务写入undo日志。
-
通过Undo Segment Header找到TRX_UNDO_LAST_LOG属性,找到链表中最后一个Undo Log Header的位置,通过undo日志将该事务对页面所做的更改全部回滚掉。