当前位置: 首页 > news >正文

数据库核心-redo、undo

一、redo日志

InnoDB操作以页为单位操作数据。并且首先操作内存中缓冲池的数据,然后刷新到disk中,但如果事务提交后宕机、未能刷新到disk中,就会造成不一致情况。
重做日志: 系统重启时按照修改步骤重新更新数据页

  • redo日志占用空间小
  • 顺序写入disk,不会随机IO

通用结构:

  • type:redo日志类型
  • space ID:表空间ID
  • page number:页号
  • data:具体内容

物理日志:记录的是内存操作,某个页面某个偏移量处修改了几个字节的值
根据修改字节数不同,又分为不同大小的日志。
但是,一个普通的insert语句,涉及的修改处不只是data的添加,还有页头信息的各种相关修改,杂七杂八,如果把这个修改点都记录下来,那这个物理redo日志是不是太多了,如果都写在一起,岂不是太冗余了,因此就有新的redo日志类型,针对不同行格式的数据产生不同的redo日志。这些日志既有物理页面意思,又有逻辑层面意思。只会把插入一条记录的必备要素记下来,之后重做时根据相关函数实现插入。

1.1Mini-Transaction

一个insert会涉及多个redo日志,但如果插入过程只记录了一部分redo日志,会导致重做结果不确定性,这是不能忍受的因此我们在执行具有原子性操作时,必须以组的形式记录日志。重做时,该组redo要么都恢复,要么都不做。
而Innodb中,有一个特殊类型的日志,该日志只有一个字段type。因此,需要保证原子性的一系列redo日志必须以该类型日志结尾。这样重做时,只要恢复到了该类型日志时,才认为完成了一个操作,不然,放弃之前恢复的其他结果。
**Mini-Transaction:**对底层页面进行一次原子访问的过程;一个MTR可以包含一组redo,用来记录某个原子操作。一条sql语句包含若干MTR;

1.2redo写入步骤

redo日志都被放在512字节的页中,类似于一个数据page,但这里称为block,同样有块头,存储基本信息,比如,块编号、已经使用多少字节等等。
同数据页,redoblock并不是直接写入disk,而是先写入bufferpool,(redo log buffer redo日志缓冲区),这片内存被划分为若干个连续的block。


写入buffer是顺序写入的,因此需要指明应该从buffer的哪个位置开始写——buf_free,该位置之后的就是空闲区;


一个事务由多个MTR组成,而一个MTR会产生多个日志,这些日志并不是产生就写入buffer,而是以MTR为单位,MTR结束时整个相关日志一起写入buffer,


刷盘时机:

  1. buffer空间不足:已经写入50%,就需要刷盘
  2. 事务提交:redo日志就是为了保持一致性,因此事务提交时,pagebuffer内容可能不会及时刷新,但redobuffer一定会及时刷新。
  3. 脏页刷新:因为特殊需求,pagebuffer中脏页刷新到disk,脏页对应的redo也需要提前刷新,并且因为redobuffer是顺序写入的,因此脏页日志前面的日志也会刷新
  4. 后台线程默认频率刷新redobuffer
  5. checkpoint刷新

磁盘中的redo日志由多个文件组成,成循环方式相连——日志文件组
日志文件组的前4个block2048字节记录管理信息,之后开始记录日志内容。


lsn-记录已经写入的redo日志量,这个值包括了head和tail,并且初始值就是8704,简单分析一下,系统刚启动,buffer刚刚初始化,lsn就要+12字节,因为虽然没日志,但第一个block的head要包括进去,然后随着工作,开始写日志,lsn不断增加,第一个block满了,就会使用第二个,lsn就要加个tail和head,不断重复下去。。。


bufferpool中会有一个flush链表,来记录被修改过的脏页信息,都是一个一个控制块,如果某个页第一次被修改,就会加入到flush链表的头部,如果之前修改过,那就修改对应控制块信息就行了;控制块中有两个信息oldest和newest,分别表示页面最早开始修改的lsn和最新修改的lsn。例如一个新的页被修改,对应的MTR在刚开始修改页时的lsn会记录在oldest中,之后修改完后,最新的lsn会记录在newest中。如果是之前修改过的页,那对应控制快的oldest不会变,只会修改newest


checkpoint: redo日志容量有限,随着bufferpool中的脏页被刷新到disk中,那么对应的redo日志占用的disk空间就没用了,就可以被重复利用。利用checkpoint_lsn表示当前系统中可以被覆盖的redo日志总量是多少。
步骤:

  1. 计算当前可以被覆盖的内存lsn最大是多少。flush链表尾部是最早修改的,该控制块的oldestlsn则表示最早的lsn,那么该lsn之前的就是可以被覆盖的!找到了可以被覆盖的lsn最大值。
  2. 将checkpoint_lsn与对应的redo日志文件组偏移量、checkpoint编号写到日志文件管理信息中。

崩溃恢复: 只需要恢复checkpoint_lsn之后的redo日志记录,虽有其中有的脏页可能已经刷新到disk中,但不确定是哪些,因为是异步进行的。
在redo日志组中第一个文件的管理信息中,两个block存储了checkpoint_lsn的信息,但我们需要最新的,而其中的checkpoint_no表示checkpoint次数,因此只需要对比两个该参数的值,大的表示记录的最新的信息,然后找到存储的checkpoint_lsn、redo日志文件组偏移量checkpoint_offset,这样起始位置就找到了,而终止位置,每个block头信息都有个对应存储量,没有存储满的就是最后一个,就找到中止位置了。

这样顺序遍历就可以恢复日志记录,但效率有点慢;
利用redo日志的spaceID和pageid计算哈希值,把两个属性相同的放在同一个槽中,因为这是操作同一个表的,放在一起效率更高,避免读取页面的随机IO,加快恢复速度。

刚才说某些脏页可能被刷新到disk中了,每个页头都一个变量表示最新修改的lsn,如果执行checkpoint,那么刷新的页中记录的改变量值就会大于整个redo日志组的checkpoint_lsn,所以处于[checkpoint_lsn,变量]之间的日志就不需要redo,而小于checkpoint_lsn的本身已经写到disk中了,因此只需要redo大于变量的那部分。

二、undo日志

redo保证了事务一致性,那么原子性呢,需要事务执行一半的操作撤销,也就是回滚——undo log

事务只有在进行增删改、创建临时表时才会分配事务id,只读事务没有事务id,默认为0

row_id,当表中没有定义主键并且没有不允许为NULL的unique键时,系统就会自动加row_id的隐藏列,而trx_id表示操作该记录的最新事务id,都很好理解了。

undo日志因为操作不同而分为不同的日志类型。

插入:
另一个隐藏列roll_point就是一个指针,指向了改记录对应的undo日志,也就表示改记录的上一个历史版本,MVCC会用到。


删除:
页面头信息会根据next_point组成一个正常记录的单向链表;同时page_free指向删除链表,表示空间可以重复利用的。而delete操作,首先会将记录删除标记位设置为1,此时仍处于正常链表中,是一个中间状态记录(delete mark)。当delete操作提交时,才会将记录移动到删除链表中的头结点处(purge)。值得注意的是,当插入记录时,首先判断删除链表头结点的空间是否可以存储新纪录,如果不可以,直接另开辟空间存储,不会遍历删除链表!如果可以,就重复利用该空间,当然,会有一小部分空间得不到利用,称为碎片空间,该部分会在整个页的删除节点都利用完了,此时才会使用,将整个链表重构,这样那些碎片空间就整理在一起了,可以被利用,但同时也会耗费性能。
而undo肯定是考虑事务未提交的情况下哎,也就只考虑delete mark阶段,


更新:
更新就情况很多了,涉及主键更新、不涉及的;更新前后大小一致的、不一致的。
如果更新前后大小一致,直接原地更新即可!如果大小不一致,会讲旧记录删掉,插入新纪录以实现更新效果;
如果不更新主键,会有一种新的undo日志类型来表示,记录各种信息。但如果更新主键,那就麻烦了,因为聚簇索引有排序,如果更新的值变化很大,索引中的位置就要变化,跨度多个页,因此首先是旧记录的deletemark,然后更新后记录的插入。事务提交之后deletemark才会执行purge,与之前的更新大小不一致做区分!


之前都是聚簇索引,如果说是二级索引,插入和删除操作差不多,但更新不一样会对二级索引b+树进行更改。


会有专门存储undo日志的page,大体结构与其他page一致,不过会有个undopageheader,这是特有的,里面记录了存储什么类型undo日志(大类,并不是前面介绍的很细致的undo类型:广义的插入和更新;因为插入undo事务提交后会删除,而更新的需要支持mvcc),写入终止位置偏移量,写入undo日志起始位置偏移量。这是从空间页的角度看undo日志的。
事务角度: 一个事务涉及多个语句,一个语句会产生多个undo日志,所以事务执行产生很多undo日志,一个page可能放不写,因此需要把同一个事务的undo日志连成链条。而之前说的undo页面有两大类分开存储,因此,一个事务有两条双向链表页面,而Innodb规定对于普通表和临时表的操作产生不同的undo日志,因此又变成了4个链表!并且并不是一次都分配空间的,而是随着事务执行,用到哪个就分配哪个,因此4个是上限,实际多少个根据事务操作而定。不同事务之间链表不同。


undolog写入过程:
首先回顾一下段的概念,逻辑上的内存划分方法,根据程序员自己需求进行划分,索引被划为为两个段-叶子节点段、非叶子节点段。逻辑上将离散的页面分成两部分。每个段有一个INODE Entry结构记录段信息,每个页面的id,段id、链表地址等等。而为了重定位这个结构,有一个Segment Header记录了哪个INODE的表空间、页号,偏移量。就能找到那个段了。


每一个undo页面链表对应一个段,链表中的节点空间都是从这个段中申请的,所以链表头节点有个undo log segment header结构,存储这个段的相关信息,一个段也叫一组。

重用undo页面:如果修改的记录很少,undo页面链表很占用空间,不值得,因此事务提交后会重用该事务的undo页面链表。能重用的页面满足两个要求:

  1. 页面链表只包含一个undo页面
  2. 页面已经使用的空间小于整个页面空间的3/4

而页面链表分为了insert和update两种,不同类型链表重用策略不一样
insert: 插入记录日志会在事务提交后就没用了,因此重用时直接覆盖原本的旧undo记录即可!
update: 这个undo日志在事务提交后不能被删除,需要支持MVCC,因此不能覆盖,在后面接着写就好。

回滚段: 我们知道一个事务执行时最多分配4个undo页面链表,为了统一管理,有一个Rollback Segment Header的页面,存储了该事务所有页面链表的first undo page页号,称为undo slot
,根据这个信息管理具体的页面链表
每个RSH都对应一个段,就是回滚段,这个段只有一个页面。

执行过程:

  1. 初始化时,没有undo日志,因此回滚段中的undo slot为null。
  2. 开始执行,需要undo日志,如果slot为null,那么就在表空间新创建一个段,用来记录页面链表,申请一个页面当做头页,更新undo slot;如果不是null,表示已经有一个页面链表了,需要跳到下一个undo slot,重复之前步骤

一个回滚段中有1024个undo slot,如果都不为null,说明都有分配了,那新事务就不能获得undo日志链表,就会报错。
当事务提交时,所有undoslot都会被处理:

  • 如果指向的页面链表可以被重用,就处于被缓存状态,两种类型链表对应两种类型的缓存链表,分别插入。
  • 如果不能被重用,再根据类型判断,如果是insert,那就释放掉;如果是update,放入history中,MVCC中的版本链!

之前说过一个回滚段只有1024个slot,显然太少了,因此随着更新,定义了128个回滚段,对应128个RSH页面,在系统表空间的第5号页面的某个区域内有128个8字节的格子,每个格子有两个属性,一个id,另一个是页号,页号就是回滚段的RSH页号,这就方便多了。这些回滚段也是有分类的,下面详细描述一下:
1. 1~32号回滚段属于一类,这些回滚段必须再临时表空间中。我们在写undo日志过程本身就是写数据,那也会产生redo日志。但对于临时表来说,修改临时表产生的undo日志只会在系统运行中有效,如果系统崩溃,重启时是不需要恢复对应页面的。因此针对临时表的undo日志,没有对应的redo日志!因此操作临时表的undo日志需要单独区分
2.0,33~127属于一类,0号回滚段必须在系统表空间中,其他的可以在系统表空间,也可以在自己配制的undo表空间中,同时写undo的过程伴随有redo日志。

MVCC中用到的回滚指针,指向就是一个undo日志,根据这个undo日志可以得到某一个版本的数据记录,构成了一个版本链。

事务执行过程中分配Undo页面链表的过程:

  1. 事务在执行过程中对普通表的记录首次做改动之前,首先会到系统表空间的第 5 号页面中分配一个回滚段(其实就是获取一个 Rollback Segment Header 页面的地址)。一旦某个回滚段被分配给了这个事务,那么之后该事务中再对普通表的记录做改动时,就不会重复分配了。使用传说中的 round-robin (循环使用)方式来分配回滚段。比如当前事务分配了第 0 号回滚段,那么下一个事务就要分配第 33 号回滚段,下下个事务就要分配第 34 号回滚段,简单一点的说就是这些回滚段被轮着分配给不同的事务(就是这么简单粗暴,没啥好说的)。
  2. 在分配到回滚段后,首先看一下这个回滚段的两个 cached链表 有没有已经缓存了的 undo slot ,比如如果事务做的是 INSERT 操作,就去回滚段对应的 insert undo cached链表 中看看有没有缓存的 undo slot ;如果事务做的是 DELETE 操作,就去回滚段对应的 update undo cached链表 中看看有没有缓存的 undoslot 。如果有缓存的 undo slot ,那么就把这个缓存的 undo slot 分配给该事务。如果没有缓存的 undo slot 可供分配,那么就要到 Rollback Segment Header 页面中找一个可用的 undoslot 分配给当前事务。
  3. 找到可用的 undo slot 后,如果该 undo slot 是从 cached链表 中获取的,那么它对应的 Undo Log Segment 已经分配了,否则的话需要重新分配一个 Undo Log Segment ,然后从该 Undo Log Segment 中申请 一个页面作为 Undo页面 链表的 first undo page 。
  4. 然后事务就可以把 undo日志 写入到上边申请的 Undo页面 链表了

最后一个问题,系统崩溃后未提交事务写的redo日志怎么办?
系统崩溃,有可能,一个未提交的事务写的redo日志已经刷盘到disk,这样系统重启后redo的话,岂不是把未提交事务做的一半更改还原了?显然违背了事务原子性,因此需要将这部分数据undo!所以需要找到那些未提交事务的id,进行undo操作。
系统重启后,首先在第5号页面的128回滚段中,看其中1024个undoslot哪些不为空,然后判断这些不为空的undo页面链表中第一个页面的undologsegmentheader的属性STATE中判断该undo页面链表是否处于活跃状态。处于活跃状态就表明,系统宕机前该事务还没有提交,就是我们需要undo的事务,根据该undo页面链表中记录的各种属性来找出对应undo日志,进行回滚!

相关文章:

  • 关于ModbusTCP/RTU协议转Ethernet/IP(CIP)协议的方案
  • 【微信小程序 onTabItemTap:精准监听 TabBar 点击事件】
  • 解锁 AI 量化新境界:Qbot 携手 iTick
  • VSCode快捷键整理
  • 【WPF】在System.Drawing.Rectangle中限制鼠标保持在Rectangle中移动?
  • Uniapp组件 Textarea 字数统计和限制
  • DeepSeekR1之四_在RAGFlow中配置DeepSeekR1模型
  • 【春招笔试真题】饿了么2025.03.07-开发岗真题
  • mac 被禁用docker ui后,如何使用lima虚拟机启动docker
  • 贪心算法--
  • C语言练习题--洛谷P生日*****(学会了新的思路)
  • leetcode日记(90)二叉树的锯齿形层序遍历
  • 【已解决】最新 Android Studio(2024.3.1版本)下载安装配置 图文超详细教程 手把手教你 小白
  • 文件操作详解(万字长文)
  • 如何检查电脑的硬盘健康状况?
  • 深入解析 C++20 中的 `std::span`:高效、安全的数据视图
  • JWT在.NET8 Webapi中的使用
  • 行为模式---状态模式
  • 【Linux】37.网络版本计算器
  • 明日直播|Go IoT 开发平台,开启万物智联新征程
  • “穿越看洪武”,明太祖及其皇后像台北故宫博物院南园展出
  • 台湾花莲县海域发生5.7级地震,震源深度15公里
  • 儿童文学作家周晴病逝,享年57岁
  • 普京称俄中关系对维护世界稳定具有战略意义
  • 爱彼迎:一季度总收入约23亿美元,将拓展住宿以外的新领域
  • “仿佛一场追星粉丝会”,老铺黄金完成国内头部商业中心全覆盖,品牌化后下一步怎么走?