原文链接:MySQL学习总结:提问式回顾 undo log 相关知识
1、redo 日志支持恢复重做,那么如果是回滚事务中的操作呢,也会有什么日志支持么?
- 也回滚已有操作,那么就是想撤销,对应的有撤销日志,也叫做 undo log。
- undo 日志分为两大类:「TRX_UNDO_INSERT」和「TRX_UNDO_UPDATE」,undo 日志需根据大类分开存储,不能混淆。
「TRX_UNDO_INSERT」对应的是insert语句、「TRX_UNDO_UPDATE」对应的是update语句和delete语句
- 问题:那如何定位事务中对哪些记录做了改动?
- 数据页记录中,有一个隐藏列「trx_id」,用于记录当前操作此记录的事务。
- MySQL会在内存维护一个全局变量,专门为事务分配事务ID。
- 每当需要为事务分配ID,则拿到上述全局变量,然后自增1。
每当上述全局变量自增到256的倍数时,需要将此值刷新到磁盘中(系统空间表页号为5的页面的 Max Trx ID 属性中)。
- 记录修改了,如何定位到对应的 undo log?
- 在记录中有一个隐藏列「roll_point」,它会指向对应的 undo 日志。
2、undo 日志都是存在哪些地方,事务对表进行编辑,怎么分配的?
- InnoDB支持128个回滚段,一个回滚段对应一个「Rollback Segment Header」页面。
- 回滚段分为两大类:第0号、第33~127号属于一类,0号存放在系统表空间、其他的可以放在系统表空间或者自己配置 undo 表空间,这类用于存放普通表改动对应的 undo 日志;第1~32号属于一类,存放在临时表空间中,这类用于存放临时表改动对应 undo 日志。
- InnoDB在系统表空间的5号页面的某个区域中,包含了128个8个字节大小的格子,用于保存128个「Rollback Segment Header」页面的地址。
-「Rollback Segment Header」页面有一个重要部分是「TRX_RSEG_UNDO_SLOTS」:它表示各个 Undo 页面链表的 first undo page 的页号集合,也叫 undo slots 集合。- 因为一个页号占用4字节,而「TRX_RSEG_UNDO_SLOTS」一共是4096个字节,所以一共可以存储1024个lot。
- 每个 slot 的初始值是 FIL_NULL,表示没有分配给其他事务使用。
- 当事务需要分配 undo 页面链表时,
- 先到回滚段对应的两个 cached 链表找是否有可用的 undo 页面;
insert undo cache 链表和 update undo cache 链表。
- 如果有则直接重用,否则需要回到「Rollback Segment Header」页中继续寻找;
- 在「Rollback Segment Header」页中便利1024个 slot,看 slot 的值是否为 FIL_NULL,如果是的话,则申请一个 undo 页面作为该 undo 页面链表的 first undo page,接着将此页面的页号赋值给当前 slot。
- 否则,表明已经有事务占用此 slot,需要继续往下寻找下一个 slot。
- 如果到了最后一个回滚段的最后一个 slot,都没有找到可用的 slot,则给客户端返回异常:Too many active concurrent transactions。
大概如下图:
3、通过上面,我们都知道「roll_point」可以定位到对应的 undo 日志,但 undo 日志也是保存在磁盘中的,那又是怎么定位的呢?
- 我们都知道聚簇索引以及二级索引,都是类型为「FIL_PAGE_INDEX」的页面;而 undo 日志也是存储在磁盘的,它对应的页面类型是「FIL_PAGE_UNDO_LOG」。
- roll_point 组成部分:
- is_insert:是否是「TRX_UNDO_INSERT」大类的 undo 日志
- rseg id:回滚段编号
- page number:指针,指向 undo 日志所在页面的页号。
- offset:指针,指向 undo 日志在页面中的偏移量。
- 所以,我们可以根据「roll_point」中的属性,找到对应的回滚段、接着找到对应的页面,最好定位到页面中的偏移量。
4、如果一个页面无法存储当前事务生成的日志,要多个页面才能完成,undo 日志间如何联系?
- undo 日志页面有一个特有的部分:「Undo Page Header」。
数据页都有一个共有的部分:「File Header」,页面间可利用链表完成联系,就是靠「File Header」 中的「FIL_PAGE_PREV」和「FIL_PAGE_NEXT」属性。
但这是页面之间的链表,如果要做到 undo 日志的链表,还需更细的连接信息。
- TRX_UNDO_PAGE_TYPE:上面提到的 und 日志分为两个大类:「TRX_UNDO_INSERT」和「TRX_UNDO_UPDATE」
- TRX_UNDO_PAGE_START:当前页面存储第一条 undo 日志的开始偏移量
- TRX_UNDO_PAGE_FREE:当前页面存储最后一条 undo 日志的结束偏移量
- TRX_UNDO_PAGE_NODE:
- Prev Node Page Number:前一个节点的页号
- Prev Node Offset:前一个节点页内的偏移量
- Next Node Page Number:后一个节点的页号
- Next Node Offset:后一个节点页内的偏移量
- 所以,可以利用每个 undo 日志页「Undo Page Header」的「TRX_UNDO_PAGE_NODE」属性来组成一个链表。
5、undo 日志也支持重用么?如果支持,如何覆盖 undo 日志?
- 重用条件:
- 首要条件:undo 页面链表对应的事务已经提交
- 第二:undo 页面链表只包含一个 undo 页面
- 第三:该页面已使用的空间小于整个页面空间的3/4
- 重用策略:
- insert undo 链表:因为对于新增记录,只要事务提交了,对应的 undo 日志就没啥用了,所以可以直接覆盖。
- update undo 链表:对于更新/删除记录,即使提交了,也不能立马删除对应的 undo 日志,因为 MVCC 需要利用此链表做文章;所以只能在后面接着写入 undo 日志,即一个 undo 页面,写入多组 undo 日志。
6、如果 undo 日志支持重用,那怎么知道从哪里开始写入第二组 undo 日志?
- undo 日志的页面有一个非常重要的部分:Undo Log Header。
- 它包含:
- TRX_UNDO_TRX_ID:本组 undo 日志对应的事务id
- TRX_UNDO_TRX_NO:事务提顺序,事务提交后会生成一个序号;先提交的序号小。
- TRX_UNDO_LOG_START:本组 undo 日志 中第一条 undo 日志在页面中的偏移量
- TRX_UNDO_NEXT_LOG:下一组 undo 日志在页面中开始的偏移量(支持 undo 日志页面复用)
- TRX_UNDO_PREV_LOG:上一组 undo 日志在页面中开始的偏移量(支持 undo 日志页面复用)
- ......
- 因此,只需拿到「TRX_UNDO_NEXT_LOG」对应的偏移量,即可知道在哪里开始继续写入第二组 undo 日志。
7、insert语句和 undo 日志
- 插入一条类型为「TRX_UNDO_INSERT_REC」 的 undo 日志,用于支持回滚操作。
- undo 日志里最主要是记录了插入的记录的主键信息:<len,value>列表
- len 为主键类型所占存储空间的长度,例如主键类型为int,占用4字节,那么len为4
- value 为主键的真实值,例如 id 列为主键,插入记录的主键 id=2,那么value为2
8、delete语句和 undo 日志
-
删除阶段:
- 插入一条类型为「TRX_UNDO_DEL_MARK_REC」的 undo 日志,用于支持回滚操作。
- 将该记录的 trx_id 和 roll_point 旧值记录到 undo 日志对应的属性中。
- delete_mark 阶段:将记录的「delete_flag」置为1,表示已经删除
- 还是保留在正常记录链表中,保留这个中间状态是为了支持MVCC
- 还会修改 trx_id、roll_point等隐藏列
- purge阶段:当事务提交时,会有专门的线程真正删除此记录
- 这里的真正删除指的是,将记录从正常记录链表中移除,加入到垃圾记录链表的表头。
- 将记录的 next_record 指向 Page Header 中的 PAGE_FREE 属性
- 将 Page Header 的 PAGE_FREE 属性执行此记录
- 修改 Page Header 中的 PAGE_GARBAGE,表示页面中可重用的字节数量
- purge 阶段不需要 undo 日志,因为此阶段在事务提交后执行。
- 这里的真正删除指的是,将记录从正常记录链表中移除,加入到垃圾记录链表的表头。
- 插入一条类型为「TRX_UNDO_DEL_MARK_REC」的 undo 日志,用于支持回滚操作。
-
扩展点:
- 每当新插入一条记录,都会先判断垃圾链表的头节点代表的已删除记录的存储空间是否满足新纪录,如果满足,直接复用。
- 疑问:如果新插入的记录一直都无法使用垃圾链表的头节点对应的存储空间,岂不是一直会存在碎片空间?下面第三点会解释!
- 如果无法满足,则需要直接向页面申请新的空间来存储。
- 但如果此时页面没有足够的空间来存储新纪录,那么就会判断「PAGE_GARBAGE」中的碎片空间和剩余的可用空间加起来是否能满足,如果可以,那么就会进行页面的重新组织
- 开辟一个临时页面,把本页面的记录依次插入
- 接着将临时页面复制到本页面,接着释放碎片空间。
- 疑问:如果「PAGE_GARBAGE」中的碎片空间和剩余的可用空间加起来都不能满足呢?是不是会有页面分裂?
- 否则,如果页面的碎片空间和剩余空间都不足以存放新纪录,那么只能进行页面分裂。
- 每当新插入一条记录,都会先判断垃圾链表的头节点代表的已删除记录的存储空间是否满足新纪录,如果满足,直接复用。
9、update语句和 undo 日志
- update 分两种情况:更新主键和不更新主键
- 不更新主键:也分为两种情况,一种是更新前后所占存储空间大小不变;另外一种是,更新前后所占存储空间变大或变小。
- 就地更新:
- 在原有记录更新
- 插入一条类型为「TRX_UNDO_UPD_EXIST_REC」的 undo 日志,用于支持回滚操作。
- 包含主键各列信息(<len,value>列表)、索引列各列信息(<pos,len,value>列表)、被更新的列更新前信息。
- 先删除再插入:
- 用户线程同步删除旧记录,更新相关的页面统计信息。
- 在当前页申请新的空间插入一条变更后的记录,如果页面剩余的存储空间不足,则需要进行页面分裂。
- 插入一条类型为「TRX_UNDO_UPD_EXIST_REC」的 undo 日志
- 就地更新:
- 更新主键:
- 对旧id记录执行删除操作,注意:这里只是执行 delete_mark 阶段,避免其他事务无法访问此记录。
插入一条类型为「TRX_UNDO_DEL_MARK_REC」的 undo 日志
- 接着根据更新后的个列值创建一条新纪录,并插入到聚簇索引中。
插入一条类型为「TRX_UNDO_INSERT_REC」 的 undo 日志