MySQL是怎样运行的——第十九章

这一章主要讲redo log。

19.1

这一节是声明,如果你前面在学习行记录格式、页面格式的时候没学好,学后面的内容可能有点难。
其实没啥事······

19.2

这一节讲什么是redo log
因为处理页都是在内存中处理,如果在提交后不刷盘,那么修改的内容就消失了。

如何解决这个问题?最朴素的办法是在事务提交之前就把更改都刷盘。这种想法是对的,但是有一些效率问题:

  • 有些浪费:不管你更改了一个页面的多少字节,都要以页为单位进行刷盘。
  • 随机IO慢:一条语句可能修改多个页面,这些页面很可能是不相邻的。这会导致大量的随机IO。

为了解决朴素办法的效率问题,我们的办法大概是每次修改记一条日志,事务提交把日志刷盘

即使之后系统崩了,按照日志重做一下即可。
redo日志每条记录体积都不大,不会浪费。同时他们是顺序写入的,没什么随机IO开销

19.3

这一节主要是格式。
先看一个通用格式:
在这里插入图片描述

  • type:redo日志的类型。一共有53种(5.7版本)
  • spaceID:表空间ID
  • page number:页号,和表空间号一起唯一确定一个页
  • data:日志具体内容

主要需要探索的就是日志的类型和内容。
先引入一个场景:

如果表没有定义主键,且没有非空唯一键,InnoDB会自动添加名为row id的列为隐藏键。
InnoDB有个全局变量,每用一个row id,变量就自增1。
每当变量变成256的整数倍时,就把它写到系统表空间的第七个页面里的名为Max Row ID属性中。
这个属性占8个字节。
之所以不是每次自增就写是为了减少磁盘io次数。
系统启动时,会把Max Row ID加载到内存里,然后+256(为了避免冲突,加256可以保证必定不会重复,只是可能有些浪费)。

这个场景其实每次更改的数据很少,只要记录一下在某个页面的某个偏移处修改了几个字节的值,修改后的内容是多少就行了。

这种很简单的redo日志记录叫做物理日志。有以下几种常见的类型:
在这里插入图片描述
其他的道理也一样,有点区别的是String,它是变长的。
在这里插入图片描述
当然,你把它的len写成8就也能实现之前的功能。但这样毕竟会比原生支持的8字节类型费空间

下面介绍一些逻辑日志,逻辑日志大概指的是日志里没有记录修改信息,只是声明了作出哪些修改。在对逻辑类型的redo log进行恢复的时候,要对逻辑日志套用一些函数,才能进行恢复。
那为什么需要逻辑日志呢?还是引入场景:

在我们向表里插入记录时,每有一个索引,就要修改一棵B+树的叶子节点。更别提还可能产生B+树的分裂了。
此外,还需要修改叶子节点甚至内部叶节点的File Header、Page Header、Page Directory等部分。
还有上一个页面的记录头的next record......
等等等等。

如果每次插入记录都要把所有更改记录下来,那日志记录就太多了。
而如果把整个页面都当作日志记录下来(就像Mit 6.830),又有点浪费。

因此产生了逻辑日志类型。下面举几个例子:
在这里插入图片描述
在这里插入图片描述
List相关的逻辑日志类型主要是防止每删除一条记录就写一行日志。可以看出设计者下很大力气去减少日志内容。
(当然这里的逻辑日志类型不是很严格的,它们有的也有物理日志的语意。只是为了和上面进行区分)。

19.4

这一节讲MTR,迷你事务。
事务有原子性,迷你事务指的是在数据库层面,一些操作应该是原子的。比如向索引里插入一条记录等。这些操作可能涉及到大量的数据页更改。(虽然前面提到了逻辑日志,但不是说所有操作只要一个逻辑日志就能搞定!)
在记录这些原子操作的日志时,应该保证原子性,也就是要么都记录,要么都不记录。我们把原子操作产生的一系列redo 日志记录叫做redo 日志组

那么如何在redo log里分组呢?答案是把每一组日志的后面加一个token作为标记。这样顺序读取,只要不读到token,就认为是一组。

如图:
在这里插入图片描述

当然,一些简单的操作,可能一条redo log就够了,没必要为它们都在后面插一个token。这种简单的记录会利用type字段做压缩。因为type字段有一个字节,类型只有53种,因此7个bit位就够描述了。我们用剩下的一个bit来标记这条redo记录是否自成一组

最后给出一个结构图:
在这里插入图片描述
我来举个例子方便大家理解:

语句是一个普通的sql。比如我们向表里插入一条记录。
mtr表示日志记录组。针对插入记录这种动作,可能要更改主索引、二级索引等。
更改每个索引都是原子的,每个更改索引动作都是一个mtr。
一个mtr里可能发生了多个页面的改动,对应大量的redo log。

19.5

这一节讲写入日志涉及到的一些组件。
日志组里的一个个日志记录会被存在磁盘上的block里。(这里的block和操作系统、磁盘本身的block有区别,其实block就是大页面)。默认block是512字节。

block的格式如下:在这里插入图片描述
如图,trailer存校验和,body存实际的日志数据,header比较复杂。

  • LOG_BLOCK_HDR_NO:block号,类似页面号。
  • LOG_BLOCK_HDR_DATA_LEN:表示用了多少字节,初始值从12开始。因为头的大小是12。
  • LOG_BLOCK_HDR_REC_GROUP:这个block里的第一个组的日志记录的偏移量。
  • LOG_BLOCK_CHECKPOINT_NO:检查点号,后面会说。
  • LOG_BLOCK_CHECKSUM:校验和

介绍完了磁盘的组件,下面介绍内存相关组件。
我们会用Buffer Pool减少磁盘IO次数,同理redo log也有对应的buffer pool,我们叫它redo log buffer。如图:
在这里插入图片描述
默认16MB。

日志记录写到redo log buffer中是顺序的,所以需要记录一下要从哪里开始写。InnoDB提供了一个变量记录当前写到什么位置了,这个变量叫buf_free。
在这里插入图片描述
假设我们有两个事务,分别是事务t1、t2。每个事务产生一组日志。(注,日志其实是一条一条产生的,在MTR没结束的时候,会把他们暂存到一个地方。MTR结束的时候统一存到log buffer里)。则一共有4组日志,分别是mtr_t1_1、mtr_t1_2,mtr_t2_1、mtr_t2_2。

因为每次写一组日志,所以效果可能是这样的:
在这里插入图片描述

19.6

这一节讲redo日志文件。
之前提到了有buffer存储redo日志文件,它们什么时候刷盘呢?

  • log buffer空间不足时。占50%左右就会触发刷盘。
  • 事务提交时。
  • 将某个脏页刷盘时,会保证对应的redo log刷到磁盘中。
  • 后台有个线程,每一秒都在把redo日志刷盘。
  • 正常关闭服务器时。
  • 做check point时(后面说)

刷盘刷到哪里呢?默认刷到数据目录(通过**SHOW VARIABLES LIKE ‘datadir’**查看)下的ib_logfile0和ib_logfile1中。如果对默认配置不满意,还可以设置:
在这里插入图片描述
刷盘时会从0号文件开始写,写到最大号。最大号写满了就会回卷。

前面说了,redo日志以block为单位。同理真正磁盘上的文件也是以block为单位的。
每个文件的前4个block存储元信息。剩下的block存log buffer里的那些日志记录。

下面看一下前四个block干嘛用的:
在这里插入图片描述
没用就是没用的······

先看header吧
在这里插入图片描述
(备注,这个头发生过不少改动)

checkpoint1和2结构完全相同,只是lsn奇数用1,偶数用2。
在这里插入图片描述

其实checkpoint有关的信息只存在于redo log磁盘文件组的第一个文件中。
具体lsn和checkpoint是什么后面会说。

19.7

这一节与lsn有关。
log sequence number,lsn。lsn用于记录一共写过多少字节的日志,初始值8704。
这个值在增长的时候会把在log buffer顺序写过程中遇到的每个block的头和尾的大小都加上
在这里插入图片描述
图片中的例子很清楚了。

最重要的结论是:每一组日志都有唯一的lsn与其对应,lsn号越小,说明日志产生的越早

前面提到了,与buf_free系统变量相关的是lsn,代表的是在内存中写redo log写到多少字节处了。同理,还有一个全局变量flushed_to_disk_lsn,用于代表redo log刷盘刷到多少字节处了。因为lsn是有偏移的,对应的无偏移系统变量是buf_next_to_write。

最开始的时候,flushed_to_disk_lsn是和lsn同步的。但因为刷盘不是实时的,所以它们的差距会变大。

看看下面的例子:
在这里插入图片描述
这是个很典型的最初的样子。
当我们刷盘两个mtr后,
在这里插入图片描述
如果两个值相同,证明都刷盘了。

lsn不止与log buffer以及磁盘日志文件组有关,还与缓冲池的flush链表有关。flush链表的控制块里存储了每个页面的最初被修改lsn和最后被修改的lsn。
在这里插入图片描述
flush链表内的节点被多次访问的时候不会移动位置。它们自然地按照页面第一次修改的时间排序。多次更新只会影响控制块里的最新lsn值。

19.8

这一节讲check point。
redo日志文件组是有限的,写满了会回写。因此我们必须知道磁盘上的文件组里哪些部分是可以被覆盖的。答案是如果日志对应的脏页已经刷到磁盘上,则这些日志就不用保留了。

我们有一个checkpoint_lsn专门用来记录脏页刷盘的进度。因为每次刷盘都是从flush链表的尾部选一个控制块,把它对应的脏页刷盘,脏页对应的lsn应该是flush链表里最小的。因此这种记录lsn的方法是可行的。变量记录的是多少,就代表该lsn以下的值都刷盘了。

这个checkpoint值也需要被刷新到磁盘上,还记得吗?我们在磁盘上的日志文件组的第一个文件的前四个block里,有checkpoint1和checkpoint2块。这两个块专门用于记录checkpoint_lsn的值。当然,也不光是记录lsn,还要记录与lsn对应的偏移,checkpoint_offset(根据lsn很好算,因为lsn与偏移是一起增长的)以及checkpoint号。这个号很简单,每产生一个检查点就+1。

哦对了,我们有checkpoint1和checkpoint2块。具体存在哪个块里是看checkpoint号的奇偶决定的。

19.9

每次从flush里刷脏页都会更新checkpoint。如果刷的页太少,flush链表就会爆满。这时候需要用户线程一起帮着刷脏页。

19.10

讲如何看各种lsn。

SHOW ENGINE INNODB STATUS;

打出来的东西很长,主要关注这几个:
在这里插入图片描述

19.11

我们最开始要求事务提交把所有脏页都刷盘,后来发现太慢。
现在要求把所有redo log都刷盘,有的人还是嫌慢·····
所以提供了一个innodb_flush_log_at_trx_commit系统变量。

  • 当变量值为0时:事务提交不要求马上把redo log刷盘,让后台线程去处理。但如果事务提交以后服务器挂了,后台线程没有把redo刷盘,那对页面的修改会丢失。
  • 当变量值为1时:默认值,刷盘
  • 当变量值为2时:把redo日志写到os的缓冲区里,不要求落盘。这样mysql挂了但是os没挂的话也不会丢数据。相当于折中方案。

19.12

主要讲崩溃恢复。
崩溃恢复第一个问题:如何确定恢复的起点?
答案是checkpoint。在checkpoint之前的脏页必定已经落盘了。具体checkpoint是多少,应该看磁盘日志文件组的第一个文件的第二个或第四个块。看着两个块中哪个块的lsn大,就从哪开始。

崩溃恢复第二个问题:如何确定恢复的终点?
答案是直接恢复到不能恢复······因为redo日志是基于块的,因此我们从前往后找,找一个没存满的块即可。
看一个块存没存满只要看log block header即可。
在这里插入图片描述
(如果刚好写满一个块就结束,则这个块的下一个块的header部分应该是0。这里的“下一个”是逻辑上的下一个,意思是可能出现回滚写)。

恢复的第三个问题:怎么恢复?
其实很简单,就是日志咋写就怎么执行…

重点是恢复过程中的加速。

加速1 哈希表

我们可以读一遍redo log,看看需要redo的页面有哪些,按页面号为key,需要redo的操作为values建立哈希表。
在这里插入图片描述
这样建立完哈希表,再遍历哈希表,每次都可以固定的恢复一个页面,避免了随机IO。

注意要保证先后顺序,必须保证redo日志里靠前的操作先被执行,也就是说哈希表的value是有顺序要求的。如果不保证顺序,哈希表可能会出错。

加速2 跳过刷到磁盘中的页面

对于lsn小于checkpoint的页面是必然已经落盘了。
但对于lsn大于checkpoint的页面,落没落盘不一定。因为在做完checkpoint以后,还有可能因后台线程啊、淘汰策略啊等等因素把某些页落盘。

在执行redo的过程中,我们肯定要把页面从磁盘上读到内存里(加载老页面),这时候要看看页面的File Header里的FIL_PAGE_LSN属性。如果这个属性大于checkpoint,意味着这个页面很新,就可以跳过这个页面,不对他进行redo。

(这里不好想,思考一下,如果一个页面的lsn小于checkpoint,对它做redo的话,相当于更新它的lsn。如果一个页面的lsn比checkpoint大,证明他不需要redo就已经很新了)

19.13

这一节讲LOG_BLOCK_HDR_NO是如何计算的。
计算方法是

(lsn / 512) & 0x3FFFFFFF + 1

意思是把lsn / 512的结果前两位变成0。这样算出来的结果一定在0-1G之间。换句话说就是最多只有1G个不重复的值。
而这个LOG_BLOCK_HDR_NO的第一个比特位很特殊,第一个位如果是1,代表这个block是在将log buffer中的block刷盘时第一个操作的block。

如果一个事务的redo日志刷新了一半的时候mysql挂了,就会出现事务执行一半的情况。这种时候需要undo日志。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值