关于MySQL redo log,我和面试官侃侃而谈了一个小时

前面文章对redo log做了基本原理介绍,本文继续对一些重要的细节做详细的研究。

Redo log:已经提交了的事务对数据库中数据所做的修改永久生效,即使后来系统崩溃,在重启后也能把这种修改恢复出来的记录。

优点:将该事务执行过程中产生的 redo 日志刷新到磁盘的好处如下:

redo 日志占用的空间非常小

存储表空间ID、页号、偏移量以及需要更新的值所需的存储空间是很小的. 

redo 日志是顺序写入磁盘的

在执行事务的过程中,每执行一条语句,就可能产生若干条 redo 日志,这些日志是按照产生的顺序写入磁盘的,也就是使用顺序IO。

一、Redo log内存结构

1.1 redo日志格式

绝大部分类型的 redo 日志都有下边这种通用的结构:

各个部分的详细释义如下: 

  •   type :该条 redo 日志的类型,InnoDB一共为 redo 日志设计了50多种不同的类型的 redo 日志。

  •   space ID :表空间ID。 

  •   page number :页号。 

  •   data :该条 redo 日志的具体内容。

把一条记录插入到一个页面时需要更改的地方非常多。

        需要插入的redo 日志既包含物理层面的意思,也包含逻辑层面的意思,具体指: 

        物理层面看,这些日志都指明了对哪个表空间的哪个页进行了修改。 

        逻辑层面看,在系统奔溃重启时,并不能直接根据这些日志里的记载,将页面内的某个偏移量处恢复成某个数据,而是需要调用一些事先准备好的函数,执行完这些函数后才可以将页面恢复成系统奔溃前的样子。

1.2 redo日志数据结构

        以类型为 MLOG_COMP_REC_INSERT 这个代表插入一条使用紧凑行格式的记录时的 redo 日志为例来理解一下我们上边所说的物理层和逻辑层。

        类型为 MLOG_COMP_REC_INSERT 的 redo 日志并没有记录 PAGE_N_DIR_SLOTS 的值修改为了啥, PAGE_HEAP_TOP 的值修改为了啥, PAGE_N_HEAP 的值修改为了啥等等这些信息,而只是把在本页面中插入一条记录所有必备的要素记了下来,之后系统奔溃重启时,服务器会调用相关向某个页面插入一条记录的那个函数,而 redo 日志中的那些数据就可以被当成是调用这个函数所需的参数,在调用完该函数后,页面中的 PAGE_N_DIR_SLOTS 、 PAGE_HEAP_TOP 、 PAGE_N_HEAP 等等的值也就都被恢复到系统奔溃前的样子了。这就是所 谓的逻辑日志的意思。

二、redo log如何写入内存

2.1 以组的形式写入redo日志

        DML sql语句在执行过程中可能修改若干个页面。例如一条 INSERT 语句可能修改系统表空间页号为 7 的页面的 Max Row ID 属性,还会更新聚簇索引和二级索引对应 B+ 树中的页面。由于对这些页面的更改都发生在 Buffer Pool 中,所以在修改完页面之后,需要记录一下相应的 redo 日志。在执行语句的过程中产生的 redo 日志被划分成了若干个不可分割的组,比如:

  •   更新 Max Row ID 属性时产生的 redo 日志是不可分割的。 

  •   向聚簇索引对应 B+ 树的页面中插入一条记录时产生的 redo 日志是不可分割的。 

  •   向某个二级索引对应 B+ 树的页面中插入一条记录时产生的 redo 日志是不可分割的。 

  •   还有其他的一些对页面的访问操作时产生的 redo 日志是不可分割的。

2.1.1 剩余空间充足

        如果该数据页的剩余的空闲空间充足,足够容纳这一条待插入记录,直接把记录插入到这个数据页中,记录一条类型为 MLOG_COMP_REC_INSERT 的 redo 日志就好了,我们把这种情况称之为乐观插入。

2.1.2 剩余空间充不足

        如果该数据页剩余的空闲空间不足,要进行所谓的页分裂操作,也就是新建一个叶子节点,然后把原先数据页中的一部分记录复制到这个新的数据页中,然后再把记录插入进去,把这个叶子节点插入到叶子节点链表中,最后还要在内节点中添加一条目录项记录指向这个新创建的页面。很显然,这个过程要对多个页面进行修改,也就意味着会产生多条 redo 日志,我们把这种情况称之为悲观插入 。

        作为内节点的页a 的剩余空闲空间也不足以容纳增加一条目录项记录 ,那需要继续做内节点页a 的分裂操作,也就意味着会修改更多的页面,从而产生更多的 redo 日志。另外,对于悲观插入来说,由于需要新申请数据页,还需要改动一些系统页面,比方说要修改各种段、区的统计信息信息,各种链表的统计信息等等,反正总共需要记录的 redo 日志有二、三十条。 

2.2 不同组区分

        针对某个组中的 redo 日志,要么把全部的日志都恢复掉,要么一条也不恢复。怎么做到的呢? 分情况讨论。

2.2.1 组内多条redo log

        需要保证原子性的操作会生成多条 redo 日志,组中的最后一条 redo 日志后边加上一条特殊类型的 redo 日志,该类型名称为 MLOG_MULTI_REC_END。

2.2.2 组内一条redo log

        需要保证原子性的操作只生成一条 redo 日志,type 字段的第一个比特位为 1 ,代表该需要保证原子性的操作只产生了单一的一条 redo 日志,否则表示该需要保证原子性的操作产生了一系列的 redo 日志。 

2.3 Mini-Transaction

        把对底层页面中的一次原子访问的过程称之为一个 Mini-Transaction ,简称 mtr ,比如上边所说的修改一次 Max Row ID 的值算是一个 Mini-Transaction ,向某个索引对应的 B+ 树中插入一条记录的过程也算是一个 Mini-Transaction。通过上边的叙述我们也知道,一个所谓的 mtr 可以包含一组 redo 日志,在进行奔溃恢复时这一组 redo 日志作为一个不可分割的整体。

        一个事务可以包含若干条语句,每一条语句其实是由若干个 mtr 组成,每一个 mtr 又可以包含若干条 redo 日 志,画个图表示它们的关系就是这样: 

2.4 redo日志的写入过程 

        为了更好的进行系统奔溃恢复,把通过 mtr 生成的 redo 日志都放在了大小为 512字节的页中。为了和表空间中的页做区别,我们这里把用来存储 redo 日志的页称为 block。

        一个 redo log block 的示意图如下:

        为了解决磁盘速度过慢的问题而引入了 Buffer Pool 。同理,写入 redo 日志时也不能直接直接写到磁盘上,实际上在服务器启动时就向操作系统申请了一大片称之为 redo log buffer 的连续内存空间,翻译成中文就是 redo日志缓冲区 ,我们也可以简称为 log buffer 。这片内存空间被划分成若干个连续的 redo log block ,就像这样:

背景知识:

  •   BufferPool,默认大小是128M

  •   Change Buffer,默认大小占Buffer Pool的25%

  •   Redo log Buffer,默认大小16M

  •   InnoDB以页为单位进行磁盘交互,一页大小默认16 KB,OS的页是4K。因此InnodB的页写入时,一个页需要分4次写

  •   机械硬盘是将数据写入扇区,一个扇区 512b,1block = 8扇区 = 4kb

  •   Innodb 以页为单位进行磁盘交互,一页大小默认16 KB

        可以通过启动参数 innodb_log_buffer_size 来指定Redo log buffer 的大小。

        每个 mtr 都会产生一组 redo 日志,用示意图来描述一下这些 mtr 产生的日志情况:

        不同的事务可能是并发执行的,所以 T1 、 T2 之间的 mtr 可能是交替执行的。每当一个 mtr 执行完成时,伴随该 mtr 生成的一组 redo 日志就需要被复制到 log buffer 中,也就是说不同事务的 mtr 可能是交替写入 log buffer 的,我们画个示意图(为了美观,我们把一个 mtr 中产生的所有的 redo 日志当作一个整体来画): 

三、redo日志文件

3.1 redo日志刷盘时机

        mtr 运行过程中产生的一组 redo 日志在 mtr 结束时会被复制到 log buffer 中,它们会在不同的时机被刷新到磁盘里,比如: 

3.1.1 log buffer 空间不足时 

        log buffer 的大小是有限的(通过系统变量 innodb_log_buffer_size 指定),如果不停的往这个有限大小的 log buffer 里塞入日志,很快它就会被填满。如果当前写入 log buffer 的redo 日志量已经占满了 log buffer 总容量的大约一半左右,就需要把这些日志刷新到磁盘上。 

3.1.2 事务提交时

        我们前边说过之所以使用 redo 日志主要是因为它占用的空间少,还是顺序写,在事务提交时可以不把修改过的 Buffer Pool 页面刷新到磁盘,但是为了保证持久性,必须要把修改这些页面对应的 redo 日志刷新到磁盘。 

3.1.3 后台线程不停的刷刷刷 

        后台有一个线程,大约每秒都会刷新一次 log buffer 中的 redo 日志到磁盘。 

3.1.4 正常关闭服务器时 

        做所谓的 checkpoint 时(后续介绍) 

3.2 redo日志文件组

        磁盘上的 redo 日志文件不只一个,而是以一个 日志文件组 的形式出现的。这些文件 以 ib_logfile[数字] (数字可以是 0 、 1 、 2 ...)的形式进行命名。在将 redo 日志写入日志文件组时,是从 ib_logfile0 开始写,如果 ib_logfile0 写满了,就接着 ib_logfile1 写,同理, ib_logfile1 写满了就去写 ib_logfile2 ,依此类推。如果写到最后一个文件该咋办?那就重新转到 ib_logfile0继续写,所以整个过程如下图所示:

         MySQL 的数据目录(使用 SHOW VARIABLES LIKE 'datadir' 查看)下默认有两个名为 ib_logfile0 和 ib_logfile1 的文件, log buffer 中的日志默认情况下就是刷新到这两个磁盘文件中。如果我们对默认的redo 日志文件不满意,可以通过下边几个启动参数来调节: 

  •   innodb_log_group_home_dir 

      该参数指定了 redo 日志文件所在的目录,默认值就是当前的数据目录。 

  •   innodb_log_file_size 

该参数指定了每个 redo 日志文件的大小,在 MySQL 5.7这个版本中的默认值为 48MB。

  •   innodb_log_files_in_group 

        该参数指定 redo 日志文件的个数,默认值为2,最大值为100。

3.3 redo日志文件格式

3.3.1磁盘基础知识回顾

硬盘(磁盘)

# fdisk -l

Disk /dev/cciss/c0d0: 146.7 GB, 146778685440 bytes

255 heads, 63 sectors/track, 17844 cylinders

Units = cylinders of 16065 * 512 = 8225280 bytes

        可以看到几个名词:heads/sectors/cylinders,分别就是磁头/扇区/柱面,每个扇区512byte(现在新的硬盘每个扇区有4K)了。硬盘容量就是heads*sectors*cylinders*512=255*63*17844*512=146771896320b=146.7G

        注意:硬盘的最小存储单位就是扇区了,而且硬盘本身并没有block和cluster的概念。

磁头和柱面

        硬盘通常由重叠的一组盘片构成,每个盘面都被划分为数目相等的磁道,并从外缘的“0”开始编号,具有相同编号的磁道形成一个圆柱,称之为磁盘的柱面。磁盘的柱面数与一个盘面上的磁道数是相等的。由于每个盘面都有自己的磁头,因此,盘面数等于总的磁头数。 如下图:

文件系统块和簇

        由于扇区的空间比较小且数目众多,在寻址时比较困难,所以操作系统就将多个的扇区组合在一起,形成一个更大的单位,再对这个单位进行整体的操作。这个单位,在Windows下,FAT,FAT32和NTFS 文件系统中叫做簇(cluster);在Linux下如Ext4等文件系统中叫做块(block)。每个簇或者块可以包括2、4、8、16、32、64…2的n次方个扇区。

  •   磁盘读写基本单位是扇区。

  •   操作系统是通过块和簇来做为单位读取等操作数据的。

  •   文件系统就是操作系统的一部分,所以文件系统操作文件的最小单位是块和簇。

  •   磁盘控制器,其作用除了读取数据、控制磁头等作用外,还有的功能就是映射扇区和磁盘块的关系。

综上,扇区是对硬盘而言,是物理层的,块和簇是对文件系统而言,是逻辑层的。磁盘控制器是用来映射两层的。

3.3.2 block件系统块

        log buffer 本质上是一片连续的内存空间,被划分成了若干个 512 字节大小的 block 。将log buffer中的redo日志刷新到磁盘的本质就是把block的镜像写入日志文件中,所以 redo 日志文件其实也是由若干个 512 字节大小的block组成。 

        redo 日志文件组中的每个文件大小都一样,格式也一样,都是由两部分组成: 

        前2048个字节,也就是前4个block是用来存储一些管理信息的。 

        从第2048字节往后是用来存储 log buffer 中的block镜像的。 

        所以我们前边所说的 循环 使用redo日志文件,其实是从每个日志文件的第2048个字节开始算,画个示意图就是这样:

        普通block的格式我们在讲 log buffer 的时候都说过了,就是 log block header 、 log block body 、 log block trialer 这三个部分,就不重复介绍了。这里需要介绍一下每个 redo 日志文件前2048个字节,也就是前4个特殊block的格式都是干嘛的,先看图: 

  • LOG_HEADER_START_LSN:标记本 redo 日志文件开始的LSN值,也就是文件偏移量为2048字节初对应的LSN值(后面介绍)。

其他属性忽略。。。

  • LOG_CHECKPOINT_NO:服务器做 checkpoint 的编号,每做一次 checkpoint ,该值就加1。

  • LOG_CHECKPOINT_LSN:服务器做 checkpoint 结束时对应的 LSN 值,系统奔溃恢复时将从该值开始。

  • LOG_CHECKPOINT_OFFSET:属性LOG_CHECKPOINT_LSN中的 LSN 值在 redo 日志文件组中的 偏移量。

其他属性忽略。

篇幅所限,下一篇讲redo log写入流程...

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值