MySQL三大日志

redo log

redo log,是一种偏向物理性质的重做日志,因为他里面记录的是类似这样的东西,“对哪个数据页中的什么记录,做了个什么修改”。用于记录事务操作的变化,记录的是数据修改之后的值,不管事务是否提交都会记录下来。
redo log里本质上记录的就是在对某个表空间的某个数据页的某个偏移量的地方修改了几个字节的值,具体修改的值是什么,他里面需要记录的就是
** 修改几个字节的值+表空间号+数据页号+偏移量+具体的值**
MLOG_1BYTE类型的日志指的就是修改了1个字节的值,MLOG_2BYTE类型的日志指的就是修改了2个字节的值,以此类推,还有修改了4个字节的值的日志类型,修改了8个字节的值的日志类型
redo log看起来大致的结构如下所示:
日志类型(就是类似MLOG_1BYTE之类的),表空间ID,数据页号,数据页中的偏移量,具体修改的数据
如果是MLOG_WRITE_STRING类型的日志,因为不知道具体修改了多少字节的数据,所以其实会多一个修改数据长度,就告诉你他这次修改了多少字节的数据,如下所示他的格式:
日志类型(MLOG_WRITE_STRING),表空间ID,数据页号,数据页中的偏移量,修改数据长度,具体修改的数据

redo log block

redo log并不是一条条直接写入磁盘中去的。在MySQL的设定中,redo log是按块,一块一块的写入到磁盘中去的。redo log block长成下面这这样:分成Header、Body、Trailer三部分 总共512字节。而且是覆盖写入
image.png

redo log buffer

redo log buffer,是MySQL专门设计了用来缓冲redo log写入的。是MySQL在启动的时候,就跟操作系统申请的一块连续内存空间,然后里面划分出了N多个空的redo log block,如下图所示。(默认大小16MB。且MySQL允许我们通过参数innodb_log_buffer_size动态的调整它。增大它的大小可以让MySQL处理大事物是不必写入磁盘。进而提升写IO性能。)
image.png
image.png
MySQL产生的redo log 先写入redo log block。然后redo log block其实就在redo log buffer 中

redo log buffer 写入磁盘时机

  1. 事务提交时把它对应的那些redo log写入到磁盘中去(可由相关参数innodb_flush_log_at_trx_commit控制)
    1. innodb_flush_log_at_trx_commit =0 依靠InnoDB 的主线程每秒执行一次刷新到磁盘
    2. innodb_flush_log_at_trx_commit =1 提交事务的时候,就必须把 redo log 从内存刷入到磁盘文件里去,只要事务提交成功,那么 redo log 就必然在磁盘里了
    3. innodb_flush_log_at_trx_commit =2 提交事务的时候,把 redo 日志写入磁盘文件对应的 os cache 缓存里去,而不是直接进入磁盘文件,可能 1 秒后才会把 os cache 里的数据写入到磁盘文件里去。提交事务的时候,把 redo 日志写入磁盘文件对应的 os cache 缓存里去,而不是直接进入磁盘文件,可能 1 秒后才会把 os cache 里的数据写入到磁盘文件里去。
  2. 当redo log buffer 使用量达到了参数innndb_log_buffer_size的一半时,会触发落盘。
  3. 会有一个后台线程,每隔1秒就会将redo log block刷新到磁盘文件中去。
  4. MySQL关闭时也会将其落盘。

redo log group

redo log group说的是:由N个大小相同的redo log(文件)组成一个redo log group。N的值默认为2。可以通过innodb_log_files_in_group修改数量
SHOW GLOBAL VARIABLES LIKE 'innodb_log%'默认单个redo log文件的大小是48MB。你也可以通过上innndb_log_files_size修改它。

bin log

binlog是记录所有数据库表结构变更(例如CREATE、ALTER TABLE…)以及表数据修改(INSERT、UPDATE、DELETE…)的二进制日志。binlog不会记录SELECT和SHOW这类操作,因为这类操作对数据本身并没有修改,但你可以通过查询通用日志来查看MySQL执行过的所有语句。

undo log

undo log主要是用来做事务回滚的。

  • 比如你执行了INSERT语句,那么你的undo log必须告诉你插入数据的主键ID,让你在回滚的时候可以从缓存页里把这条数据给删除了;
  • 如果你执行了DELETE语句,那么你的undo log必须记录下来被删除的数据,回滚的-时候就得重新插入一条数据;
  • 如果你执行了UPDATE语句,那么你必须记录下来修改之前的数据,回滚的时候就得把数据给更新回去

undo log也是记录在数据页中,该数据页的类型为FIL_PAGE_UNDO_LOG,这种类型的数据页存放的是undo log日志。这些页面可以从系统表空间中分配, 也可以从一种专门存放undo日志的表空间, 一般每对一条记录做一次改动, 就对应着一条undo日志, 但在某些更新记录的操作中, 也可能会对应着2条undo日志(详见UPDATE操作更新主键)。
同一个Undo页面要么只存储TRX_UNDO_INSERT大类的undo日志, 要么只存储TRX_UNDO_UPDATE大类的undo日志, 反正不能混着存, 所以在一个事务执行过程中就可能需要2个Undo页面的链表, 一个称之为insert undo链表, 另一个称之为update undo链表

  • undo log页格式

image.png

  • 隐藏列

聚簇索引的记录除了会保存完整的用户数据以外, 而且还会自动添加名trx_idroll_pointer的隐藏列, 如果用户没有在表中定义主键以及UNIQUE键, 还会自动添加一个名为row_id的隐藏列。

  • trx_id列是某个对这个聚簇索引记录做改动的语句所在的事务对应的事务id
  • roll_pointer本质就是一个指针, 指向记录对应的undo日志
    image.png

INSERT操作对应的undo日志

insertundo日志类型为TRX_UNDO_INSERT_REC
image.png

DELETE操作对应的undo日志

删除记录的步骤

数据页中正常的记录会根据记录头信息中的next_record属性组成一个单向链表, 被删除的记录(被删除的记录不会真正删除,而是给记录打上标记)也根据记录头信息中的next_record属性组成一个链表, 只不过这个链表中的记录占用的存储空间可以被重新利用, 所以也称这个链表为垃圾链表。 Page Header部分有一个称之为PAGE_FREE的属性, 它指向由被删除记录组成的垃圾链表中的头节点。
image.png
删除记录需要两个阶段

  1. 仅仅将记录的delete_mask标识位设置为1, 其他的不做修改

image.png

  1. 当该删除语句所在的事务提交之后, 会有专门的线程后来真正的把记录删除掉。 所谓真正的删除就是把该记录从正常记录链表中移除, 并且加入到垃
    圾链表中
    image.png

删除记录的回滚日志

deleteundo日志的类型为TRX_UNDO_DEL_MARK_REC
image.png

UPDATE操作对应的undo日志

在执行UPDATE语句时, InnoDB对更新主键和不更新主键这两种情况有截然不同的处理方案。

不更新主键的情况

针对UPDATE不更新主键的 场景,undo日志的类型为TRX_UNDO_UPD_EXIST_REC
image.png

  • 就地更新

更新记录时, 对于被更新的每个列来说, 如果更新后的列和更新前的列占用的存储空间都一样大, 那么就可以进行就地更新, 也就是直接在原记录的基础上修改对应列的值。

  • 先删除掉旧记录, 再插入新记录

在不更新主键的情况下, 如果有任何一个被更新的列更新前和更新后占用的存储空间大小不一致, 那么就需要先把这条旧的记录从聚簇索引页面中删除掉, 然后再根据更新后列的值创建一条新的记录插入到页面中(这里所说的删除并不是delete mark操作, 而是真正的删除掉, 也就是把这条记录从正常记录链表中移除并加入到垃圾链表中 )

更新主键的情况

在聚簇索引中, 记录是按照主键值的大小连成了一个单向链表的, 如果我们更新了某条记录的主键值, 意味着这条记录在聚簇索引中的位置将会发生改变 ,此时更新分为两步

  1. 将旧记录进行delete mark操作
  2. 根据更新后各列的值创建一条新记录, 并将其插入到聚簇索引中

因此,针对UPDATE语句更新记录主键值的这种情况, 在对该记录进行delete mark操作前, 会记录一条类型为TRX_UNDO_DEL_MARK_RECundo日志; 之后插入新记录时,会记录一条类型为TRX_UNDO_INSERT_RECundo日志, 也就是说每对一条记录的主键值做改动时, 会记录2条undo日志。

undo log写入过程

每一个Undo页面链表都对应着一个段, 称之为Undo Log Segment。 也就是说链表中的页面都是从这个段里边申请的, 所以他们在Undo页面链表的第一个页面(段的概念参考MySQL数据结构)
Undo链表的第一个页面比普通页面多了个Undo Log Segment Header
image.png

undo log页面链表状态

TRX_UNDO_STATE:记录 本Undo页面链表处在什么状态。
一个Undo Log Segment可能处在的状态包括:

  1. TRX_UNDO_ACTIVE活跃状态, 也就是一个活跃的事务正在往这个段里边写入undo日志。
  2. TRX_UNDO_CACHED被缓存的状态。 处在该状态的Undo页面链表等待着之后被其他事务重用。
  3. TRX_UNDO_TO_FREE: 对于insert undo链表来说, 如果在它对应的事务提交之后,** 该链表不能被重用**, 那么就会处于这种状态。
  4. TRX_UNDO_TO_PURGE: 对于update undo链表来说, 如果在它对应的事务提交之后, 该链表不能被重用, 那么就会处于这种状态。
  5. TRX_UNDO_PREPARED: 包含处于PREPARE阶段的事务产生的undo日志。

链表的基节点

undo页面的Undo Page Header部分有一个12字节大小的TRX_UNDO_PAGE_NODE属性, 这个属性代表一个List Node结构。 每一个Undo页面都包含Undo PageHeader结构, 这些页面就可以通过这个属性连成一个链表。 这个TRX_UNDO_PAGE_LIST属性代表着这个链表的基节点, 当然这个基节点只存在于Undo页面链表的第一个页面, 也就是first undo page
image.png

写入过程

一个事务在向Undo页面中写入undo日志时的方式是十分简单暴力的, 就是直接往里怼, 写完一条紧接着写另一条, 各条undo日志之间是亲密无间的。 写完一个Undo页面后, 再从段里申请一个新页面, 然后把这个页面插入到Undo页面链表中, 继续往这个新申请的页面中写。 同一个事务向一个Undo页面链表中写入的undo日志算是一个组

重用Undo页面

一个Undo页面链表是否可以被重用的条件

  1. 该链表中只包含一个Undo页面。

如果一个事务执行过程中产生了非常多的undo日志, 那么它可能申请非常多的页面加入到Undo页面链表中。 在该事物提交后, 如果将整个链表中的页面都重用, 那就意味着即使新的事务并没有向该Undo页面链表中写入很多undo日志, 那该链表中也得维护非常多的页面, 那些用不到的页面也不能被别的事务所使用, 这样就造成了另一种浪费。 只有在Undo页面链表中只包含一个Undo页面时, 该链表才可以被下一个事务所重用。

  1. Undo页面已经使用的空间小于整个页面空间的3/4。
  2. insert undo链表

insert undo链表中只存储类型为TRX_UNDO_INSERT_RECundo日志, 这种类型的undo日志在事务提交之后就没用了, 就可以被清除掉。 所以在某个事务提交后, 重用这个事务的insert undo链表( 这个链表中只有一个页面) 时, 可以直接把之前事务写入的一组undo日志覆盖掉, 从头开始写入新事务的一组undo日志,
但是update undo链表不能被重用
在一个事务提交后, 它的update undo链表中的undo日志也不能立即删除掉(MVCC会用到)。所以如果之后的事务想重用update undo链表时,就不能覆盖之前事务写入的undo日志。 这样就相当于在同一个Undo页面中写入了多组的undo日志。

回滚段

一个事务在执行过程中最多可以分配4个Undo页面链表(系统表两个(INSERT,UPDATE),临时表两个), 在同一时刻不同事务拥有的Undo页面链表是不一样的, 所以在同一时刻系统里有许多Undo页面链表存在。 为了管理这些链表, 设计InnoDB设计了一个称之为Rollback Segment Header的页面,每一个Rollback Segment Header页面都对应着一个段, 这个段就称为Rollback Segment, 翻译过来就是**回滚段 **

image.png

  • TRX_RSEG_MAX_SIZE: 本Rollback Segment中管理的所有Undo页面链表中的Undo页面数量之和的最大值。
  • 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页面链表的first undo page的页号集合, 也就是undo slot集合。一个页号占用4个字节, 对于16KB大小的页面来说, 这个TRX_RSEG_UNDO_SLOTS部分共存储了1024undo slot, 所以共需1024 × 4 = 4096个字节

从回滚段中申请Undo页面链表

  1. 初始情况下, 由于未向任何事务分配任何Undo页面链表, 所以对于一个Rollback Segment Header页面来说, 它的各个undo slot都被设置成了一个特殊的值: FIL_NULL(对应的十六进制就是0xFFFFFFFF) , 表示该undo slot不指向任何页面。
  2. 随着时间的流逝, 开始有事务需要分配Undo页面链表了, 就从回滚段的第一个undo slot开始, 看看该undo slot的值是不是FIL_NULL:
    • 如果是FIL_NULL, 那么在表空间中新创建一个段(也就是Undo Log Segment) , 然后从段里申请一个页面作为Undo页面链表的first undo page, 然后把该undo slot的值设置为刚刚申请的这个页面的地址, 这样也就意味着这个undo slot被分配给了这个事务。
    • 如果不是FIL_NULL, 说明该undo slot已经指向了一个undo链表, 也就是说这个undo slot已经被别的事务占用了, 那就跳到下一个undo slot, 判断该undo slot的值是不是FIL_NULL, 重复上边的步骤。
  3. 一个Rollback Segment Header页面中包含1024个undo slot, 如果这1024个undo slot的值都不为FIL_NULL, 这就意味着这1024个undo slot都已经名花有主(被分配给了某个事务) , 此时由于新事务无法再获得新的Undo页面链表, 就会回滚这个事务并且给用户报错:

Too many active concurrent transactions

事务提交时undo slot的去向

  1. 如果该undo slot指向的Undo页面链表符合被重用的条件(就是我们上边说的Undo页面链表只占用一个页面并且已使用空间小于整个页面的3/4) 。该undo slot就处于被缓存的状态, InnoDB规定这时该Undo页面链表的TRX_UNDO_STATE属性会被设置为TRX_UNDO_CACHED。被缓存的undo slot都会被加入到一个链表, 根据对应的Undo页面链表的类型不同, 也会被加入到不同的链表:
  2. 如果对应的Undo页面链表是insert undo链表, 则该undo slot会被加入insert undo cached链表。
  3. 如果对应的Undo页面链表是update undo链表, 则该undo slot会被加入update undo cached链表。

一个回滚段就对应着上述两个cached链表, 如果有新事务要分配undo slot时, 先从对应的**cached**链表中找。 如果没有被缓存的**undo slot**, 才会到回滚段的**RollbackSegment Header**页面中再去找。

  • 如果该undo slot指向的Undo页面链表不符合被重用的条件, 那么针对该undo slot对应的Undo页面链表类型不同, 也会有不同的处理:
  • 如果对应的Undo页面链表是insert undo链表, 则该Undo页面链表的TRX_UNDO_STATE属性会被设置为TRX_UNDO_TO_FREE, 之后该Undo页面链表对应的段会被释放掉(也就意味着段中的页面可以被挪作他用) , 然后把该undo slot的值设置为FIL_NULL。如果对应的Undo页面链表是update undo链表, 则该Undo页面链表的TRX_UNDO_STATE属性会被设置为TRX_UNDO_TO_PRUGE, 则会将该undo slot的值设置为FIL_NULL, 然后将本次事务写入的一组undo日志放到所谓的History链表中

多个回滚段

早期的MySQL版本只有1个回滚段,后来的版本定义了128个回滚段, 也就相当于有了128 × 1024 = 131072个undo slot ,每个回滚段都对应着一个Rollback Segment Header页面, 有128个回滚段, 也就是有128个Rollback Segment Header页面,InnoDB在系统表空间的第5号页面的某个区域包含了128个8字节大小的格子
image.png
每个8字节的格子的构造就像这样
image.png
每个8字节的格子其实由两部分组成:

  • 4字节大小的Space ID, 代表一个表空间的ID。
  • 4字节大小的Page number, 代表一个页号。

也就是说每个8字节大小的格子相当于一个指针, 指向某个表空间中的某个页面, 这些页面就是Rollback Segment Header。 这里需要注意的一点事, 要定位一个RollbackSegment Header还需要知道对应的表空间ID, 这也就意味着不同的回滚段可能分布在不同的表空间中。
系统表空间的第5号页面中存储了128个Rollback Segment Header页面地址, 每个Rollback Segment Header就相当于一个回滚段。 在Rollback Segment Header页面中, 又包含1024个undo slot, 每个undo slot都对应一个Undo页面链表。 示意图:
image.png

为事务分配Undo页面链表详细过程

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

配置回滚段数量

系统中一共有128个回滚段, 其实这只是默认值, 我们可以通过启动参数innodb_rollback_segments来配置回滚段的数量, 可配置的范围是1~128。 但是这个参数并不会影响针对临时表的回滚段数量, 针对临时表的回滚段数量一直是32, 也就是说:

  1. 如果我们把innodb_rollback_segments的值设置为1, 那么只会有1个针对普通表的可用回滚段, 但是仍然有32个针对临时表的可用回滚段。
  2. 如果我们把innodb_rollback_segments的值设置为2~33之间的数, 效果和将其设置为1是一样的。
  3. 如果我们把innodb_rollback_segments设置为大于33的数, 那么针对普通表的可用回滚段数量就是该值减去32。
posted @ 2023-08-14 17:21  yangleduo114  阅读(61)  评论(0)    收藏  举报