MySQL的undo日志—MVCC前置知识

undo日志

前面学习了redo日志,redo日志保证的是崩溃时事务持久性。我们可以从redo日志恢复到系统崩溃以前。

undo日志就是为了保证事务回滚时事务所作所为都能回到事务执行前。保证了事务的原子性。redo把我们做增删改之前的状态记录下来,帮助MySQL回滚到事务执行之前的样子。

这篇文章了解一下事务ID和undo日志产生就OK了,对于Undo日志存储可以直接略过。

事务ID

有两种类型的事务:只读事务和读写事务。

[En]

There are two types of transactions: read-only transactions and read-write transactions.

针对于只读事务,MySQL会在其对用户创建的临时表进行增删改的时候才会为其分配事务ID,否则不分配。

这里的临时表指的是create temporary 表名,和我们使用explain SQL的时候在extra上显示的using temporary不一样。前者是用户创建的用户临时表,只针对于当前session有效,后者是MySQL内部临时表。

而针对于读写事务来说,MySQL会在事务执行对某个表进行增删改的时候为其分配一个事务ID,否则不分配。

事务ID生成

在系统启动时,系统维护一个全局变量,我们首先从内存中找到MAX TRX ID这个值然后加上256,赋给这个全局变量。

每次出现上述情况的事务时会为其分配一个ID,然后变量进行+1操作。

然后每每这个全局变量是256的倍数时候就会对这个变量进行同步修改到系统表空间中的MAX TRX ID属性处。

然后我们为什么在取值的时候要加上 256呢?主要是因为我们系统关闭是可能已经大于当前MAX TRX ID但是还没有到256的倍数,所以我们只要将其+256,就会得到一个唯一的事务ID咯。

trx_id隐藏列

在介绍数据行时,我们已经提到了数据行的三个隐藏列。

[En]

When we introduced the data row, we already mentioned three hidden columns of the data row.

  • row_id 就当我们没有主键或者unique列的时候,会生成一个这个唯一的row_id来保持记录的唯一性
  • trx_id 事务ID
  • roll_pointer 后面MVCC的时候介绍,这里链着一个版本链🤭

undo日志类型

为了保证原子性,所以Innodb对每个增删改都会在改之间进行一次undo日志的记录。

下面就来介绍一下增、删、改也就是insert、delete、update会产生的undo日志类型以及其细节吧。

INSERT的undo日志

我们使用insert的SQL语句时,就会产生一个TRX_UNDO_INSERT_REC类型的undo日志。

MySQL的undo日志---MVCC前置知识
  • end of record 和 start of record就是指向尾部和指向头部的地址。
  • undo type 就是TRX_UNDO_INSERT_REC
  • undo no 就是undo日志的no,对于每个事务来说这个值会从0开始,每个事务维护一个no从0开始慢慢+1递增。
  • table id就是这个日志所在的表id
  • 主键信息。多列就是多列。
    [En]

    Primary key information. Multiple columns are multiple.*

所以innodb回滚会发生什么?首先呢,我们插入可能是悲观插入和乐观插入,悲观就是页满了得进行分裂,乐观就是没满直接将数据行插入。但是呢?innodb做的还是得到插入的主键值,然后删除主键值对应的聚簇索引和二级索引。

当我们执行insert插入时,插入的数据行中的隐藏列,我们关心的主要是事务ID和roll_pointer这两个属性。

  • 因为我们执行的是插入,所以执行语句的事务会生成一个唯一的事务ID。后面我们将插入,修改,删除都会有一个唯一的事务ID,就不必多讲了。
  • roll_pointer指向的就是innodb生成的undo日志。

MySQL的undo日志---MVCC前置知识

上图我们在一个事务中先后执行了2次插入操作,它的roll_pointer都指向了对应生成的undo日志。后续将介绍存储undo的页面。

DELETE的undo日志

delete操作就稍微有点特殊了。

我们在前面介绍页面的时候介绍了一个数据行的next_record属性,就是指向下一个数据行的指针。我们也说过这些被删除的数据行是可以被重用的,它其实是被一个存储在PAGE_HEADER中的一个PAGE_FREE的属性给链起来了,也就是说被删除的垃圾页面会形成一个链表。

MySQL的undo日志---MVCC前置知识

我们在介绍数据行的时候也介绍了一个delete_mask位,用来标记数据行是否被删除。

在交易中,删除分为两个阶段。

[En]

In the transaction, deletion is divided into two stages.

  1. delete_mask变为1,但是却没有加入垃圾链表,成为了一个中间状态的行记录

MySQL的undo日志---MVCC前置知识
  1. 然后在执行该语句的事务提交的时候,会有专门的线程来将其放入垃圾链表中,然后修改页头中的记录信息。这个阶段称为purge。

MySQL的undo日志---MVCC前置知识

之所以有这个垃圾链表,就是为了让接下来插入的数据行用来重用的,如果不能用来重用,这个垃圾链表简直没有意义。
同时呢我们需要注意到,新删除的记录是放在垃圾链表的第一个的,可能是只有PAGE_FREE头部地址是被记录的,所以我们只能用头插法插入到链表中。
重用流程:
首先数据行要插入到页中,先检查垃圾链表头的大小是不是 >=插入的数据行的大小,如果是就直接将垃圾链表头拉出来覆盖重用。否则就重新创建。
它没有错,它只检查垃圾链头,其他什么都不检查,而且容易碎片化,因为它大于或等于重用的条件。

[En]

There is nothing wrong with it, it only checks the junk chain header, nothing else is checked, and it is prone to fragmentation, because it is greater than or equal to the condition for reuse.

当没有足够的空间插入新记录时,检查垃圾链表中是否有足够的空间和碎片来放下记录,以便复制页面上的有用数据,这是无奈的。通过这种方式,可以删除碎片和不可用的垃圾列表。

[En]

When there is not enough space to insert a new record, check that there is enough space in the junk linked list and debris to put down the record, so that the useful data on the page can be copied, which is helpless. In this way, fragments and unavailable junk lists can be removed.

我们在改变中间状态时,会生成一个TRX_UNDO_DEL_MARK_REC类型的undo日志。

MySQL的undo日志---MVCC前置知识

我们需要注意的是它将旧的事务ID和roll_pointer记录下来了,以及主键和索引列的信息。

所以会出现下图的情况,出现了一个版本链,原本是指向了插入的undo日志,然后删除的中间状态下,删除的undo日志还会指向插入的undo日志。

MySQL的undo日志---MVCC前置知识

具体的删除undo日志就是如下

MySQL的undo日志---MVCC前置知识

值得注意的是,索引栏的每一栏的信息

[En]

It is worth noting that the information of each column of the index column

update的undo日志

update有两种情况

  • 不更新主键
  • 更新主键

不更新主键

如果更新的不是主键列。

旧记录  ("a","15")
新纪录  ("b","20") 大小没有发生改变
新纪录  ("aa0","20") 第一个列大小发生了改变
  • 当更新后的数据行 每列的大小不发生改变,强调一下是每列的大小都不发生改变。我们就会使用就地更新,直接将更新后的数据行覆盖到旧的数据行。
  • 当更新后的数据行列的大小发生改变,我们就不能就地更新。我们将使用用户线程去删除旧记录,即直接delete_mark置为1然后直接放进垃圾链表中,没有像delete语句那样还弄什么中间状态和调用其他线程去删除数据行。然后将新数据行加入到页中,同时如果小于垃圾链表第一个的大小还是重用删除页。

不更新主键的情况下会插入的undo日志如下:

MySQL的undo日志---MVCC前置知识

更新主键

如果我们更新主键,我们的操作将完全不同。

[En]

If we update the primary key, our operation will be completely different.

  1. 我们将记录直接将旧的数据行的delete_mark其标记成1,但不放入垃圾链表中的中间状态,然后插入类型为TRX_UNDO_DEL_MARK_REC的undo日志,在事务提交时就会有专门的线程做purge操作。
  2. 然后我们就可以直接将新的数据页插入到其中,然后插入类型为TRX_UNDO_INSERT_REC的undo记录,记录下旧的数据行的信息。

更新主键的情况下会产生2条undo日志。

为什么要这样呢?主要是因为如果我们在更新的时候,并发的事务要读取这条旧的数据,为了防止脏读,就需要将其置为中间状态,让其他事务同样也可以读到这条旧的数据。如果我们直接删除了其他事务就读不到了,不就脏读了嘛。

UNDO日志的存储

接下来就是介绍undo日志会存放在哪里了。。十分的枯燥,而且就到处引来引去。而且感觉讲得云里雾里

通用链表结构

这是链表中每个节点的结构,即首页页码+页码偏移量和后续页码+页码偏移量

[En]

This is the structure of each node in the linked list, the front page number + page offset and the subsequent page number + page offset

MySQL的undo日志---MVCC前置知识

为了方便链表的管理,有链表的基节点,指向链表的头节点和尾节点,记录链表的长度。

[En]

To facilitate the management of linked lists, there are the base nodes of the linked list, pointing to the head and tail nodes, and recording the length of the linked list.

MySQL的undo日志---MVCC前置知识

链表本链在此。

MySQL的undo日志---MVCC前置知识

FIL_PAGE_UNDO_LOG页面

我们前面在图片中出现了一次这个undo日志的存储页面。

MySQL的undo日志---MVCC前置知识

我们接下来介绍一下这个undo日志的通用页面。File Header 和 File Trailer就不用介绍了吧,老演员了,和前面介绍的页面是一样的。

MySQL的undo日志---MVCC前置知识
  • TRX_UNDO_PAGE_TYEP 这个就是指什么种类的undo日志

我们在上面提到了增删改会产生的undo日志,其中呢,我们可以分成两个种类。

  • TRX_UNDO_INSERT : 类型为TRX_UNDO_INSERT_REC的undo日志,一般在insert语句产生,还有就是update更新主键的时候
  • TRX_UNDO_UPDATE : 除了TRX_UNDO_INSERT_REC类型的undo日志,其他类型都这个种类的。

  • TRX_UNDO_PAGE_START 指的就是当前页面undo日志从哪里开始

  • TRX_UNDO_PAGE_FREE 指的是当前页面空闲的地址。

MySQL的undo日志---MVCC前置知识
  • TRX_UNDO_PAGE_NODE 代表一个LIST NODE的结构,会有一个指向前后的指针。就是页面之间形成链表的结构。

UNDO页面链表

当一个事务中,我们生成的undo日志过多了,一个页面肯定是放不下,我们就会创建多个页面进行存储,然后我们使用链表将其链起来。

MySQL的undo日志---MVCC前置知识

我们将undo页面链表的第一个页面叫做first undo page ,其他页面叫做normal undo page。

我们上面也提过的,就是我们可以将undo日志分成两个大类,一个是TRX_UNDO_INSERT 和TRX_UNDO_UPDATE ,不同种类的undo日志会存储到不同的undo页面中。所以呢,我们会为普通表和临时表各自维护这两个大类的页面。

MySQL的undo日志---MVCC前置知识

但是呢,这个undo链表只有当前事务执行SQL创建了相应种类的undo日志我们才会去创建这个链表,并不是一开始就创建。

对于不同事务来说,不同事务有不同的undo页面链表。

MySQL的undo日志---MVCC前置知识

我们前面讲过在B+树的根节点存储了Segment Header 结构,就是存储INODE ENTRY的位置。

MySQL的undo日志---MVCC前置知识

我们在上面提到的undo页面链表的结构,第一个undo页面我们叫做first undo page。

因为first undo page有点不一样,它被设计存储了一个Undo Log Segment Header 的部分,用来表示对应段的Segment Header信息以及关于段的其他信息。

说实话, 没懂这个结构有个dio用,就TRX_UNDO_LAST_LOG 在后面有点用,其他的都是一句就过了。

MySQL的undo日志---MVCC前置知识

我们再来看看Undo Log Segment Header 里面存放了什么。

MySQL的undo日志---MVCC前置知识
  • TRX_UNDO_STATE 该undo页面俩表处于什么状态
  • TRX_UNDO_ACTIVE 活跃状态,表示活跃事务正在写入undo日志
  • TRX_UNDO_CACHED 缓存状态,表示该undo页面正在等待被其他事务重用。
  • TRX_UNDO_TO_FREE 对于insert undo链表来说,如果在事务被提交时,此链表不能重用就会处于这个状态。
  • TRX_UNDO_TO_PURGE 对于update undo链表来说,事务提交时,页面不能重用就会进入这个状态。
  • TRX_UNDO_PREPARED 包含处于PREPARE阶段的事务产生的日志。分布式事务会出现该状态。
  • TRX_UNDO_LAST_LOG 本Undo页面链表的最后一个Undo Log Header的位置。这个还好理解一点在看完全部章节之后,因为页面会重用嘛。
  • TRX_UNDO_FSEG_HEADER 本Undo页面链表对应的段的Segment Header信息,一个undo链表也是被一个段维护起来了。
  • TRX_UNDO_PAGE_LIST Undo页面链表的基节点

好乱真的无语,作者写的这章我真😵了,我看了3遍都还不懂。上面那个结构完全不知道要干什么。

在Undo Log Segment Header下面存储了Undo Log Header块

MySQL的undo日志---MVCC前置知识

MySQL的undo日志---MVCC前置知识

MySQL的undo日志---MVCC前置知识

重用undo页面

重用的条件

  • 该链表中只包含一个Undo页面。
  • 该Undo页面已经使用的空间小于整个页面空间的3/4

insert undo链表的重用,对于这个链表当满足上述两个条件时,会直接覆盖重用undo页面。因为插入旧的undo日志在事务提交后旧没有用了。

MySQL的undo日志---MVCC前置知识

update undo链表的重用,对于这个链表满足上述两个条件时,会在旧的undo页面中的free处继续写。因为MVCC需要用到旧的undo日志的,是不能覆盖的。

MySQL的undo日志---MVCC前置知识

回滚段

相当于将每个事务维护的first undo page给集合起来,放到一个页面中,这个页面就叫回滚段。

MySQL的undo日志---MVCC前置知识
  • TRX_RSEG_MAX_SIZE : 这个回滚段维护第一页链表中的undo页面的最大值。
  • TRX_RSEG_HISTORY_SIZE : history链表的占用的页面数量
  • TRX_RSEG_HISTORY : history链表的基节点
  • TRX_RSEG_FSEG_HEADER : 回滚段对应的段的位置INODE Entry
  • TRX_RSEG_UNDO_SLOTS : 各个first undo page 的位置的集合,也叫undo slot集合。

从回滚段中申请一个undo slot

undo slot 中的TRX_RSEG_UNDO_SLOTS 每4字节都是一个默认值FIL_NULL。

当事务需要创建一个undo链表,就会向回滚段申请一个undo slot。 回滚段就会顺序往下找,找到值为FIL_NULL的4字节地址,然后undo链表申请了第一个页面的地址放入到undo slot 中,就是将其地址改变为申请的页面的地址。

当事务提交时,我们需要判断这个slot是不是能被重用:

  • 如果能重用根据其种类加入到insert undo cache或者update undo cache链表中。
  • 如果不能重用呢
  • 如果是种类insert种类的就会将其TRX_UNDO_STATE 设置为FREE的状态,这个页面就被释放掉了,该页面能作为其他的undo日志用。
  • 如果种类是update的就会将其TRX_UNDO_STATE 设置为purge状态,然后这个undo链表头会被放到History链表中。不能删除因为MVCC要用。

多个回滚段

就是当事务多起来了,每个事务都由4个链表要维护,就要4个undo slot ,一个回滚段就不够,旧版本就一个回滚段。

因此,在SYSTEM表空间中应用了128个8字节正方形。

[En]

So 128 8-byte squares are applied in the system tablespace.

MySQL的undo日志---MVCC前置知识

MySQL的undo日志---MVCC前置知识

因为这个space ID就指表空间ID,Page Number就是对应表空间的页号。这个表空间ID就表示不同的回滚段可能位于不同的表空间中。这样我们就有128*1024个undo slot 可以分配使用了。

MySQL的undo日志---MVCC前置知识

回滚段的分类

第0号,第33-127号回滚段属于第一类。第0号一定在系统表空间中,他们是针对普通表的undo slot 进行分配。

第1-32号回滚段属于第二类。是分配给临时表的undo slot。无论我们怎么修改回滚段的大小,这32个是一定在的。

在修改针对普通表的回滚段时,对于页面的修改是需要redo日志来记录的,而对于临时表的修改是不需要写redo日志的。

事务分配Undo页面链表过程

  1. 首先向系统表空间申请一个回滚段 回滚段是循环使用的,就是从0 、33-127这几个位置,就循环分配给事务,防止都分配一个回滚段炸了。
  2. 先从cache链表中找到对应类型的有没有可以重用的,有就直接将重用链表拿出来重用。没有下一步。
  3. 找到一个没被占用的undo slot ,将申请first undo page 位置填到对应的地方。
  4. 然后就可以插入undo日志了。

回滚段的配置

mysql> show variables like 'innodb_rollback_segments';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_rollback_segments | 128   |
+--------------------------+-------+
1 row in set, 1 warning (0.04 sec)

我们可以更改回滚段的大小,但临时表的32个回滚段无论有多小都在那里。

[En]

We can change the size of the rollback segment, but the 32 rollback segments of the temporary table are there no matter how small.

后面文章总结MVCC,这算是MVCC的一个前置知识。

Original: https://www.cnblogs.com/duizhangz/p/16333565.html
Author: 大队长11
Title: MySQL的undo日志—MVCC前置知识

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/505330/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球