《MySQL是怎么运行的:从根儿上理解MySQL》(21-22)学习总结

说明

文章的图片来源《MySQL是怎么运行的:从根儿上理解MySQL》,本篇文章只是个人学习总结,欢迎大家买一本正版小册看看,对于mysql是由浅入深的讲解非常细致

21.redo日志(下)

redo日志文件

redo日志刷盘时机

  • log buffer内存不够的时候
  • 事务提交的时候
  • 后台线程不停刷,每秒会刷新一次
  • 关闭服务器的时候

redo日志文件组

  • innodb_log_group_home_dir:redo日志所在的目录
  • innodb_log_file_size:每个redo日志大小,默认是48MB
  • innodb_log_files_in_group:日志文件组通常是2个(ib_logfile1,2…)
  • 如果写完一个文件怎么办?就会重新从第一个文件开始写

image-20211106142324489

redo日志文件格式

两部分组成

  • 前2048个字节也就是4个block用来管理信息
  • 往后的字节都是用来存储log buffer上面的block镜像(这个其实就是log buffer上的block)的

image-20211106142703592

  • redo日志的前四个block

image-20211106142731740

  • 对于log file header的属性
    • LOG_HEADER_FORMAT:redo日志版本,通常是1
    • LOG_HEADER_PAD1:字节填充
    • LOG_HEADER_START_LSN :标记日志文件开始的LSN值
    • LOG_HEADER_CREATOR:文件的创建者,通常是mysql的版本号
    • LOG_BLOCK_CHECKSUM :校验block值

image-20211106142759057

  • checkpoint1
    • LOG_CHECKPOINT_NO :编号,每做一次checkpoint就加1
    • LOG_CHECKPOINT_LSN:做checkpoint结束的LSN值,如果系统崩溃就从这里开始
    • LOG_CHECKPOINT_OFFSET:LSN值在文件组偏移量
    • LOG_CHECKPOINT_LOG_BUF_SIZE:在做checkpoint的log buffer的大小
    • LOG_BLOCK_CHECKSUM:本block的校验值

image-20211106143024765

Log Sequeue Number

  • 日志序列号就是LSN,初始是8704
  • 第一次初始化log buffer,这个时候12个字节是管理信息头,buf_free会指向第一个可以写的地方,也就是12字节偏移处,并且LSN也会+12

image-20211106143647501

  • 如果mtr产生的redo日志比较小,LSN增加的数量就是mtr产生日志的字节数

image-20211106143724408

  • 如果mtr产生的日志比较大的时候,除了日志占用空间还需要加上log block header和log block tailer的占用的字节数

image-20211106143906958

  • 每组mtr都有对应的LSN,LSN越小,说明日志产生的时间就越早

flushed_to_disk_lsn

  • buf_next_to_write:记录的是刷新到的位置,也就是它前面的日志都已经刷新到磁盘了
  • flushed_to_disk_lsn:lsn是系统写入到redo log上面的日志序列号,但是flushed_to_disk_lsn就是刷新到redo log上面日志序列号,和lsn并不会同步

image-20211106144206682

  • 下面是演示,系统写入3个mtr。然后没有刷新到磁盘的时候。lsn和flushed_to_disk_lsn拉开了差距

image-20211106144622479

image-20211106144726239

lsn值和redo日志文件偏移量的对应关系

  • 计算LSN的文件偏移位置是非常简单的。

image-20211106145933573

flush链表中的LSN

  • mtr修改完页面之后还需要把对应的脏页写到flush链表头部上
  • 脏页节点在flush链表上是按照时间从大到小排列的,两个关于页面何时修改的属性
    • oldest_modification:第一次被修改的时候,mtr开始时的lsn写到这里
    • newest_modification:每一次修改都把mtr结束的lsn写入到这里

image-20211106150132309

  • mtr1修改了a页面,开始的时候lsn是8716,结束的时候是8916

image-20211106150546502

  • mtr2同时修改b和c,开始的lsn是8916,结束的时候9948。

image-20211106150609973

  • 后面mtr3修改了d和b,b由于之前修改过,只需要修改lsn最后的位置就可以了,并不需要重新插入链表的头部。

image-20211106150757493

总结

对于mtr的每次修改,第一次修改插入到flush头部并且记录开始的lsn和结束修改的lsn。如果是多次更新就不会重复加入到flush链表头部而是直接更新最后一次的lsn结束位置

checkpoint

明确redo日志的目的:为了系统崩溃的时候恢复脏页(内存修改的页面,但是没有更新到磁盘),如果已经更新到了磁盘,那么redo日志就已经失去了作用。这些空间又可以被其它的更新操作使用。

  • 这个时候4个脏页面还没有刷新到磁盘,所以不能够被覆盖使用

image-20211106151227885

  • 下面是a页面刷新到了磁盘,所以磁盘里面的mtr1相关的日志已经没有作用了。可以被覆盖使用

image-20211106151318245

  • checkpoint_lsn:表示当前系统可以被覆盖的redo日志总量,初始还是8704

  • 页面a如果被刷新了,那么checkpoint_lsn就可以增加,这个过程就是checkpoint,两个步骤

    • 计算当前系统被覆盖的redo日志对应的lsn最大是多少
      • 计算出最早被修改的脏页的oldest_modification,lsn小于它的都可以被覆盖。把值赋予给checkpoint_lsn,比如现在的a刷新了,那么就只剩下c了,就把它的oldest_modification作为checkpoint_lsn的新值
      • 把checkpoint_lsn、checkpoint_no和checkpoint在文件的偏移量写到redo日志文件组的管理信息中,通常checkpoint有1和2,奇数放到1,偶数放到2
  • 相当于就是flushed_to_dist_lsn管理的是刷新到磁盘的log,lsn是最新写入的log,checkpoint是脏页写到磁盘已经可以被覆盖的log。

image-20211106153927708

批量从flush链表中刷出脏页

  • 如果修改数据太多,导致写入的日志非常多,快速占满了log buffer,这个时候就需要用户线程去把flush链表最早的脏页批量刷新到磁盘,让checkpoint及时让出位置来加入新的log。

查看系统中的各种LSN值

  • Log sequence number:系统的lsn值,已经写入系统的日志量
  • Log flushed up to:flushed_to_disk_lsn写入磁盘的日志量
  • Pages flushed up to:flush链表最早修改的那个oldest_modification属性值
  • Last checkpoint at:当前系统的checkpoint_lsn值

innodb_flush_log_at_trx_commit的用法

如果每次事务提交都需要把redo日志提交到磁盘上面,就会耗费很大的性能,innodb_flush_log_at_trx_commit系统变量可以改变策略

  • 0:提交不会立刻刷新到磁盘,而是等待后台线程来进行刷新
  • 1:提交的时候就要把日志同步到磁盘
  • 2:事务提交,日志写到redo日志的缓冲区,不需要一定要写入到磁盘,因为申请的内存在操作系统,所以mysql挂了没事,只要操作系统没挂

崩溃恢复

恢复过程

确定恢复起点

  • 从checkpoint_lsn开始,这个lsn之前都是已经可以被覆盖的,后面的就是要么还没有刷新到磁盘,要么就是已经刷新到磁盘的log
  • 两个block保存了checkpoint的信息,选取最近发生的checkpoint,早晚可以通过checkpoint_no来进行确定。并且求出checkpoint的offset

确定恢复的终点

  • 普通的log block header有LOG_BLOCK_HDR_DATA_LEN记录在该block使用了多少个字节,只需要扫描最后一个使用的字节不是512的block那么就是最新更新的block的结尾,获取这里的lsn。

image-20211106155308292

怎么恢复

image-20211106155456564

  • 扫描checkpoint到最新lsn的位置,并且调用函数进行恢复操作。但是可以加快
    • 哈希表:表空间id和page number相同的放到同一个位置,链表串起来,每次都可以一下子刷新掉这个链表的页的所有修改,并且按照时间顺序进行恢复

image-20211106155637190

  • 跳过已经刷新到磁盘的页面

因为在chepoint的时候,由于是多线程操作,checkpoint同时也可能有新的脏页刷新到磁盘,问题是如何知道脏页已经刷新到磁盘?每个页面的File Header的FIL_PAGE_LSN记录最近一次的修改lsn,如果被刷新了lsn肯定大于checkpoint_lsn。这种页面就不需要根据redo日志进行恢复

LOG_BLOCK_HDR_NO是如何计算的

  • log block header上面有LOG_BLOCK_HDR_NO代表一个唯一的标号((lsn / 512) & 0x3FFFFFFFUL) + 1
  • ((lsn / 512) & 0x3FFFFFFFUL) + 1这个值在00x3FFFFFFFUL之间,加1之后就是10x40000000UL,最多只能产生1GB的不重复的block块

总结

  • redo日志文件的一个刷新时机
    • 提交事务
    • 后台线程
  • redo日志文件通过文件组的方式进行存储,可以有多个这样的问题
  • redo日志文件的格式,通过block为单位进行存储,都有header和tailer
  • LSN是redo日志文件的关键,包括控制chekpoint位置(也就是日志对应的脏页已经刷新到磁盘不需要再使用),另外还有flushed_to_disk_lsn(刷新到磁盘的redo日志),还有就是仅仅只是刷新到缓存区的lsn。这里的缓存区是在操作系统上面的,所以服务器崩溃并不会影响存储在缓存区的redo日志。
  • 最后就是崩溃恢复的步骤
    • 起点是在checkpoint位置
    • 终点就是block没有满512字节的位置
    • 最后就是可以通过给指定的内容和参数给函数,来进行恢复。

22.undo日志(上)

事务回滚的需求

  • 情景1:事务执行到一半,出现错误
  • 情景2:手动回滚
  • 所以需要让事务看起来什么都没做,就需要进行回滚

回滚之前的操作

  • 插入需要记录主键值,才能回滚删除
  • 删除需要保存记录的内容,才能回滚插入
  • 修改需要记录旧值

记录这些东西的就是undo log,回滚日志

事务id

给事务分配id的时机

  • START TRANSACTION READ ONLY开启一个只读事务,对临时表进行增删改
  • START TRANSACTION READ WRITE开启一个读写事务

如果事务对表进行了增删改就需要加上一个事务id

  • 对于只读事务来说,用户创建临时表在增删改才会为事务分配事务id
  • 读写事务:对表第一次增删改才会为事务分配id

事务id是怎么生成的

  • 维护一个全局变量,变量自增并且赋值
  • 如果变量到达256的倍数的时候,去到5页面更新Max Trx ID
  • 系统下次启动就会把Max Trx ID加载进内存,并且加上256赋值给全局变量,因为全局变量可能比上一次的Max Trx ID更大,如果不这样赋值可能会有重复的事务id。

trx_id隐藏列

  • 这里的id的意思就是记录做改动所在的事务的事务id

image-20211106163307548

undo日志的格式

  • undo_no就是每条日志的编号
  • FIL_PAGE_UNDO_LOG这个就是undo log的页面类型从系统表空间上面去分配

INSERT操作对应的undo日志

接下来就是使用这个表

CREATE TABLE undo_demo

( id INT NOT NULL,

key1 VARCHAR(100),

col VARCHAR(100),

PRIMARY KEY (id),

KEY idx_key1 (key1) )Engine=InnoDB CHARSET=utf8;

TRX_UNDO_INSERT_REC

image-20211106164818233

  • undo_no:每生成一条undo日志,这个日志的no就会加1
  • 如果主键只有一个列,那么就直接存储这个列的值和存储空间,因为删除操作会把聚簇和二级索引的值同时删除。

现在插入两条数据到undo_demo

  • 然后就会有两条undo_log数据,分别记录了,表的id,主键的存储大小和值。

image-20211106182003054

image-20211106182118669

roll_pointer

实际上就是指向记录对应的undo日志。日志被存放到FIL_PAGE_UNDO_LOG类的页面上。

  • 每个记录都会有自己的一条undo日志,也就是会对应着一个roll_pointer指针。

image-20211106182416124

DELETE操作对应的undo日志

  • 正常记录有自己的一个记录链表,同样被删除的节点也有自己的一个链表
  • 并且被Page Header的Page Free指向已经删除记录的头结点

image-20211106182516338

现在删除最后一条记录过程

  • 阶段1:把delete_mask标识为1,这个阶段被称为delete mark,这种是中间状态

image-20211106182824808

删除事务还没有提交那么就会处于这种中间状态,没有转移到垃圾链表

  • 阶段2:事务提交之后会有线程专门进行处理,删除记录放到垃圾链表,还要修改PAGE_N_RECS、PAGE_LAST_INSERT上次插入的位置、PAGE_FREE垃圾链表头结点、PAGE_GARBAGE页面可以使用的字节,这个过程是purge

image-20211106183255776

  • 被删除的节点实际上是会被放到垃圾链表的头结点
  • PAGE_GARBAGE的作用其实就是可以用于处理空间碎片问题
    • 如果新插入的记录空间大于这个PAGE FREE指向的垃圾链表头节点,那么就重新向页面请求空间。
    • 如果是小于等于就可以直接重用垃圾链表头结点。并且把节点迁移到正常记录的链表
    • 如果实在没有空间,但是PAGE_GARBAGE有足够空间,那么就重新组织所有的链表,并且整理出空间给新的记录

TRX_UNDO_DEL_MARK_REC

image-20211106183829821

image-20211106183838289

  • 在delete mark之前需要把trx_id和roll_pointer的值记录到undo日志,现在是先插入一条记录,这个时候记录的roll_pointer肯定就是指向insert这条undo日志,也就是记录了insert的这个主键和值方便回滚删除这条记录。那么现在又执行了一个删除操作,那么就需要新记录的roll_pointer指向delete undo这条新的undo日志。并且在这之前还需要保存之前的insert undo日志。并且让delete undo的roll_pointer指向insert_undo方便之后的回滚。也就是日志之间做成了一条链表,这条链表就是版本链

image-20211106184152432

  • TRX_UNDO_DEL_MARK_REC还增加了各个索引列的信息,实际上就是在索引上的索引列,需要保存值,索引的pos位置,还需要保存len长度。

下面就是插入之后再删除的一个版本链

image-20211106185402881

  • 删除undo日志是第三条日志,因为之前插入了两条记录。所以现在它是trx_id=100事务的产生的第三条日志

  • 记录的最近一次事务是id=100(最近一次修改本记录的事务就是本删除事务),所以存入old trx_id中,而且隐藏列roll_pointer是指向insert这条回滚日志,所以也给删除日志的old roll_pointer进行赋值,这样就可以找到最近的一次修改产生undo log

    • 只要是被包含在索引的索引列都需要存入pos(索引第几个列)、len(列占用的字节数)、value(值)
    • 下面是id是int,所以value占用四个字节

image-20211106190115496

  • 对于idx_key1来说就是
    • pos:在id、trx_id、roll_pointer列之后,pos的值是3
    • len:varchar(100),使用utf-8,最后awm占用了3个字节
    • value:value要使用3个字节进行存储

image-20211106190435137

  • 最后就是把所有索引列占用的空间字节写到index_col_info len属性

UPDATE操作对应的undo日志

不更新主键的情况

分为更新的列存储空间是否发生变化。

  • 就地更新

这种更新的前提是更新之后的占用字节数和当前的占用字节数相同,也就是存储空间在更新前后没有发生变化,那么就可以在当前节点直接进行更新

image-20211106191734580

  • 删除旧的,插入新的

这里的删除是真正删除,直接删除掉聚簇索引页面上的记录,修改PAGE_FREE和PAGE_GARBAGE。如果新记录能重用垃圾链表的节点,那么就使用,如果不能,那么就只能重新申请了。

TRX_UNDO_UPD_EXIST_REC

image-20211106192442559

image-20211106192451665

  • n_updated表示有多少个列被更新了,<pos, old_len, old_value>
  • 如果更新的列包含索引列,那么也是需要把索引列各项信息填上去的。

下面这个是插入之后删除然后再更新

BEGIN; # 显式开启一个事务,假设该事务的id为100
# 插入两条记录
INSERT INTO undo_demo(id, key1, col)
VALUES (1, 'AWM', '狙击枪'), (2, 'M416', '步枪');
# 删除一条记录
DELETE FROM undo_demo WHERE id = 1;
# 更新一条记录
UPDATE undo_demo
SET key1 = 'M249', col = '机枪'
WHERE id = 2;

image-20211106192625195

image-20211106192641781

  • 这个是id=100事务做的一个处理,而且是第4个产生的日志
  • roll_pointer指向那条undo_no是1的日志。最近一次的修改。
  • 更新了key1,那么就要写入到更新列和索引列上。并且索引列还需要记录主键的列。

更新主键的情况

  • 旧记录做一个delete mark,提交后purge,删除会记录一条TRX_UNDO_DEL_MARK_REC的undo日志

  • 根据更新信息创建新的一条记录插入,接着就是做一条TRX_UNDO_INSERT_REC的undo日志。

  • 也就是主键改动需要多记录两条日志

DATE undo_demo
SET key1 = ‘M249’, col = ‘机枪’
WHERE id = 2;


[外链图片转存中...(img-jB33sb0k-1636338466195)]

[外链图片转存中...(img-txVB8B9l-1636338466196)]

- 这个是id=100事务做的一个处理,而且是第4个产生的日志
- roll_pointer指向那条undo_no是1的日志。最近一次的修改。
- 更新了key1,那么就要写入到更新列和索引列上。并且索引列还需要记录主键的列。

### 更新主键的情况

- 旧记录做一个delete mark,提交后purge,删除会记录一条TRX_UNDO_DEL_MARK_REC的undo日志

- 根据更新信息创建新的一条记录插入,接着就是做一条TRX_UNDO_INSERT_REC的undo日志。
- 也就是主键改动需要多记录两条日志

おすすめ

転載: blog.csdn.net/m0_46388866/article/details/121202962