系列文章:MySQL系列专栏mysql
经过前面的文章咱们已经了解到数据增删改的一个大体过程以下:web
表空间ID
以及在表空间中的数据页的页号
表空间ID+页号
做为Key,去缓存页哈希表
中查找Buffer Pool
是否已经加载了这个缓存页。若是已经加载了缓存页,就直接读取这个缓存页。Free链表
获取一个空闲页加入LRU链表
中,加载的数据页就会放到这个空闲的缓存页中。Flush链表
中。LRU链表
尾部的冷数据和Flush链表
中的脏页刷盘。这个过程有个最大的问题就是,数据修改且事务已经提交了,但只是修改了Buffer Pool中的缓存页,数据并无持久化到磁盘,若是此时数据库宕机,那数据不就丢失了!sql
可是也不可能每次事务一提交,就把事务更新的缓存页都刷新回磁盘文件里去,由于缓存页刷新到磁盘文件里是随机磁盘读写
,性能是不好的,这会致使数据库性能和并发能力都不好。数据库
因此此时就引入了一个 redo log
机制,在提交事务的时候,先把对缓存页的修改以日志的形式,写到 redo log 文件
里去,并且保证写入文件成功才算事务提交成功。并且redo log
是顺序写入
磁盘文件,每次都是追加
到磁盘文件末尾去,速度是很是快的。以后再在某个时机将修改的缓存页刷入磁盘,这时就算数据库宕机,也能够利用redo log
来恢复数据。缓存
这就是MySQL里常常说到的WAL
技术,WAL 的全称是Write-Ahead Logging
,它的关键点就是先写日志,再写磁盘
。服务器
redo log
本质上记录的就是对某个表空间的某个数据页的某个偏移量的地方修改了几个字节的值,它须要记录的其实就是 表空间号+数据页号+偏移量+修改的长度+具体的值,因此 redo log 占用的空间很是小,一条 redo log 也就几个字节到几十个字节的样子。markdown
针对不对的修改场景,InnoDB定义了多种类型的 redo log,不一样类型的 redo log 基本上就是下面这样的一个结构。数据结构
日志类型就有50多种,其中最简单的几种类型就是根据修改了几个字节的值来划分的:并发
MLOG_1BYTE
:修改了1字节的值。MLOG_2BYTE
:修改了2字节的值。MLOG_4BYTE
:修改了4字节的值。MLOG_8BYTE
:修改了8字节的值。MLOG_WRITE_STRING
:写入一串数据。MLOG_WRITE_STRING
类型的 redo log 表示写入一串数据,可是由于不能肯定写入的数据占多少字节,因此须要在日志结构中添加一个长度字段来表示写入了多长的数据。高并发
除此以外,还有一些复杂的redo log类型来记录一些复杂的操做。例如插入一条数据,并不只仅只是在数据页中插入一条数据,还可能会致使数据页和索引页的分裂,可能要修改数据页中的头信息(Page Header)、目录槽信息(Page Directory)等等。
例以下面的一些复杂日志类型:
MLOG_REC_INSERT
:插入一条非紧凑行格式的记录的 redo log。MLOG_COMP_REC_INSERT
:插入一条紧凑行格式的记录的 redo log。MLOG_COMP_REC_DELETE
::删除一条使用紧凑行格式记录的 redo log。MLOG_COMP_PAGE_CREATE
:建立一个存储紧凑行格式记录的页面的 redo log。关于日志格式咱们知道这么多就好了,对日志的结构和类型有个大概的认识就能够了。
一个事务中可能有多个增删改的SQL语句,而一个SQL语句在执行过程当中可能修改若干个页面,会有多个操做。
例如一个 INSERT 语句:
若是表没有主键,会去更新内存中的Max Row ID
属性,并在其值为256
的倍数时,将其刷新到系统表空间
的页号为7
的Max Row ID
属性处。
接着向聚簇索引插入数据,这个过程要根据索引找到要插入的缓存页位置,向数据页插入记录。这个过程还可能会涉及数据页和索引页的分裂,那就会增长或修改一些缓存页,移动页中的记录。
若是有二级索引,还会向二级索引中插入记录。
最后还可能要改动一些系统页面,好比要修改各类段、区的统计信息,各类链表的统计信息等等。
也就是说一个SQL语句在底层可能会有不少操做,会记录不少条 redo log
,可是一些操做是不可分割的,是一个原子的。例如向聚簇索引插入记录,这个操做是不可分割,不能只完成其中一部分。
因此InnoDB将执行语句的过程当中产生的redo log
划分红了若干个不可分割的组,一组redo log
就是对底层页面的一次原子访问,这个原子访问也称为 Mini-Transaction
,简称 mtr
。一个 mtr
就包含一组redo log
,在崩溃恢复时这一组redo log
就是一个不可分割的总体。
一个事务能够包含若干条SQL语句,每一条SQL语句实际上是由若干个mtr
组成,每个mtr
又能够包含若干条redo log
,看起来就是下图所示的结构。
redo log
并非一条一条写入磁盘的日志文件中的,并且一个原子操做的 mtr
包含一组 redo log
,一条一条的写就没法保证写磁盘的原子性了。
InnoDB设计了一个 redo log block
的数据结构,称为重作日志块(block
),重作日志块跟缓存页有点相似,只不过日志块记录的是一条条 redo log。一个 mtr
中的 redo log 其实是先写到一个地方,而后再将一个 mtr
的日志记录复制到block
中,最后在一些时机将block
刷新到磁盘日志文件中。
一个 redo log block
固定 512字节
大小,由三个部分组成:12字节
的header块头,496字节
的body块体,4字节
的trailer块尾。redo log 就是存放在 body 块体中,也就是一个块实际只有 496字节
用来存储 redo log。
block header
块头记录了四个信息:
LOG_BLOCK_HDR_NO
:表示块的惟一编号。
LOG_BLOCK_HDR_DATA_LEN
:表示 block 中已经使用了多少字节,初始值为12
,由于body
从第12
个字节处开始。若是block body
已经被所有写满,那么本属性的值就被设置为512
。
LOG_BLOCK_FIRST_REC_GROUP
:表示block中第一个mtr
日志组中的第一条 redo log 的偏移量。
LOG_BLOCK_CHECKPOINT_NO
:表示 checkpoint 的序号,后面会介绍。
block trailer
只记录了一个信息:
LOG_BLOCK_CHECKSUM
:表示block的校验值。跟 Buffer Pool
相似的,服务器启动时,就会申请一块连续的内存空间,做为 redo log block
的缓冲区也就是 redo log buffer
。而后这片内存空间会被划分红若干个连续的 redo log block
,redo log 就是先写到 redo log buffer 中的 redo log block 中的。
能够经过启动参数innodb_log_buffer_size
来指定log buffer
的大小,该参数的默认值为16MB
。
mysql> SHOW VARIABLES LIKE 'innodb_log_buffer_size';
+------------------------+----------+
| Variable_name | Value |
+------------------------+----------+
| innodb_log_buffer_size | 16777216 |
+------------------------+----------+
复制代码
redo log
是以一个 mtr
为单位写入 block 中的,多个事务并发执行可能会有多组mtr
,也就是说不一样事务的 mtr
可能会交叉写入 block 中。
好比有两个事务T一、T2:
看起来可能就像下图这样,两个事务中的两组mtr
交叉写入block中,每一个mtr的大小也不同,有些大的mtr甚至会占超出一个block的大小。
图中还有一个buf_free
,这是InnoDB设计的一个全局变量,用来指向 log buffer 中能够写入log的位置。
log block 跟 Buffer Pool 中的缓存页同样,会在一些时机刷入磁盘中。
主要有下面的一些时机会刷盘:
若是写入 log buffer
的日志占据了 log buffer
总容量的一半了,默认状况下也就是超过8MB
的时候,此时就会把他们刷入到磁盘文件里去。
这种状况通常在高并发的场景下可能会出现,每秒执行了不少增删改SQL语句,产生的redo log 瞬间超过了8M
,而后就立马触发刷新 log block 到磁盘。不过这种状况通常比较少。
一个事务提交的时候,必须把它的redo log
都刷入到磁盘文件里去,只有这样,才能保证事务的持久性,才算事务提交成功了(这就是force log at commit
机制,即在事务提交的时候,必须先将该事务的全部事务日志写入到磁盘上的日志文件中进行持久化)。若是在写入的过程当中MySQL宕机了,那事务也就失败了。
好比前面的事务T2的 redo log 占据了3个block,在提交T2事务时,就必须把这3个block都刷入磁盘。
后台线程刷盘:后台有一个线程会每隔1秒
,把redo log block
刷到磁盘文件里去。
MySQL关闭的时候,redo log block
都会刷入到磁盘里去。
作 checkpoint
的时候。这个后面会说。
须要注意的是,无论什么时机刷盘,redo log block
始终是顺序刷盘
的,好比事务提交的时候,会把这个事务mtr以前的block都刷入磁盘。
好比下面的T一、T2事务,在事务T1提交的时候,虽然事务T2还没完成,但会把图中箭头所指的位置以前的block都刷入磁盘。这个刷盘是时时刻刻都在进行的,因此一次刷盘也不会有不少block。
在提交事务的时候,InnoDB会根据配置的策略来将 redo log 刷盘,这个参数能够经过 innodb_flush_log_at_trx_commit
来配置。
能够配置以下几个值:
0
:事务提交时不会当即向磁盘中同步 redo log,而是由后台线程来刷。这种策略能够提高数据库的性能,但事务的持久性
没法保证。
1
:事务提交时会将 redo log 刷到磁盘,这能够保证事务的持久性,这也是默认值。其实数据会先写到操做系统的缓冲区(os cache),这种策略会调用 fsync
强制将 os cache 中的数据刷到磁盘。
2
:事务提交时会将 redo log 写到操做系统的缓冲区中,可能隔一小段时间后才会从系统缓冲区同步到磁盘文件。这种状况下,若是机器宕机了,而系统缓冲区中的数据还没同步到磁盘的话,就会丢失数据。
为了保证事务的持久性
,通常使用默认值,将 innodb_flush_log_at_trx_commit
设置为1
便可。
MySQL会不停的执行增删改SQL语句,而后不断的产生 redo log,那这么多 redo log 不可能所有存到磁盘文件中。其实也不必,由于 redo log 只是用来恢复数据的,那已经持久化到表空间的数据就不会用 redo log 来恢复了,也就是说可用的 redo log 的量实际上是比较少的。下面来看下 redo log 是如何写入磁盘文件的。
redo log 会写入一个目录下的日志文件中,实际上是一组日志文件。
这个目录默认就是数据目录,能够经过以下命令查看:
mysql> SHOW VARIABLES LIKE 'datadir';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| datadir | /var/lib/mysql/ |
+---------------+-----------------+
复制代码
默认在数据目录下能够看到有 ib_logfile0
、ib_logfile1
两个文件,这就是一组日志文件。默认一组中有两个日志文件,文件名的格式为 ib_logfile[x]
(x
为从0
开始的数字)。
咱们能够经过以下参数来调整 log buffer 的配置:
innodb_log_buffer_size
:指定 redo log buffer 的大小,默认为 16MB
。innodb_log_group_home_dir
:指定redo log文件所在的目录,默认值就是当前的数据目录。innodb_log_file_size
:指定每一个redo log文件的大小,默认值为48MB
。innodb_log_files_in_group
:指定redo log文件的个数,默认值为2
,最大值为100
。mysql> SHOW VARIABLES LIKE 'innodb_log_%';
+-----------------------------+----------+
| Variable_name | Value |
+-----------------------------+----------+
| innodb_log_buffer_size | 16777216 |
| innodb_log_checksums | ON |
| innodb_log_compressed_pages | ON |
| innodb_log_file_size | 50331648 |
| innodb_log_files_in_group | 2 |
| innodb_log_group_home_dir | ./ |
| innodb_log_write_ahead_size | 8192 |
+-----------------------------+----------+
复制代码
在将 redo log 写入日志文件组时,是从 ib_logfile0
开始写,若是 ib_logfile0
写满了,就接着ib_logfile1
写,ib_logfile1
写满了就去写 ib_logfile2
,依此类推。若是写到最后一个文件也满了,就会从新转到ib_logfile0
覆盖写入。
整个过程以下图所示:
前面已经知道,redo log 是先写入 redo log buffer 中的 redo log block 中的,而后事务提交时,会将 log block 写入磁盘中的 redo log 文件。redo log 文件是一组日志文件,默认在数据目录下就有两个 48MB
的日志文件。
log block 固定为512字节
大小,redo log 文件也是同样按512字节
来划分的,每一个 redo log 文件的格式也是同样的,都由若干个512字节
的块组成。
每一个 redo log 文件由两部分组成:
前2048字节
,也就是前4个block
是用来存储一些管理信息。其中第1个 block 存储文件头信息
,第2个和第4个存储checkpoint
,第3个block保留未没用。
从第2048字节日后是用来存储 redo log block 的。
因此在循环写日志文件的时候,实际上是从每一个日志文件的第2048字节
开始的。但须要注意的是,一组日志文件中,只有第1个日志文件的前4个block才会存储管理信息,其他的日志文件只是保留这些空间,不存储信息。
其中,文件头信息和两个checkpoint包含的信息以下图所示。
header
中的各个属性:
LOG_HEADER_FORMAT
:redo日志的版本LOG_HEADER_PAD1
:作字节填充用的,没什么实际意义LOG_HEADER_START_LSN
:标记本日志文件开始的LSN
值,初始值就2048
,指向文件偏移量2048字节
处。LOG_HEADER_CREATOR
:标记本日志文件的建立者。LOG_BLOCK_CHECKSUM
:本block的校验值checkpoint
中的各个属性:
LOG_CHECKPOINT_NO
:服务器作checkpoint
的编号,每作一次checkpoint,该值就加1
。LOG_CHECKPOINT_LSN
:服务器作checkpoint
结束时对应的LSN
值,系统崩溃恢复时将从该值开始。LOG_CHECKPOINT_OFFSET
:上个属性中的LSN值在redo日志文件组中的偏移量。LOG_CHECKPOINT_LOG_BUF_SIZE
:服务器在作checkpoint操做时对应的log buffer
的大小。LOG_BLOCK_CHECKSUM
:本block的校验值。前面已经知道,redo log 是循环写入日志文件组中的,那么就会有个问题,如何保证哪些 redo log 是能够被覆盖的呢?redo log 是用来恢复数据的,其实只要 redo log 对应的脏页已经刷到磁盘了,那这部分 redo log 就没用了。那恢复数据的时候又应该恢复哪部分数据呢?这一切都和LSN
有关系。
InnoDB设计了一个全局变量 Log Sequence Number
,简称 LSN
,就是日志序列号
的意思。LSN就表明写入的日志总量,LSN 的初始值是 8704
,占用8
个字节,且是单调递增的。
仍是之前面T一、T2事务为例,假设T一、T2事务产生的mtr大小以下:
120字节
,mtr_T1_2 200字节
。862字节
,跨了3个block,mtr_T2_2 100字节
。LSN 不只包含 redo log 的大小,还包含了 block 的块头和块尾。下面这张图就展现了伴随着T一、T2事务mtr的写入,LSN的变化状况。
能够看出,每一组mtr
都有一个惟一的LSN
值与其对应,LSN 值越小,说明对应mtr
中的redo log
产生的越早。
事务产生的mtr
写入log block
后,会将修改的脏页加入到Flush链表
头部,Flush链表对应的描述信息块中会有两个属性来记录LSN信息:
oldest_modification
:记录mtr开始的LSN值。newest_modification
:记录mtr结束时的LSN值。接着另外一个mtr写入后,可能Flush链表中已经存在了对应的脏页,此时会将mtr结束时
的LSN值写入newest_modification
,本来的oldest_modification
则保持不变。
实际上Flush链表
中的脏页就是按照修改发生的时间顺序进行排序,也就是按照oldest_modification
表明的LSN值进行排序的。链表靠近尾部的是最先修改的,链表头部则是最新修改的。
前面介绍过数据页的结构,在它的File Header
中有一个属性 FIL_PAGE_LSN
,它表示页面最后被修改时的日志序列位置LSN
。这个属性在用 redo log 来恢复数据的时候也起着重要的做用。
在事务中执行增删改SQL语句时,会更新LRU链表
中的缓存页,而后将这些缓存页加入Flush链表
的头部,在向log block
中写入一个mtr
后,就会将最新的LSN值写入所在页中的FIL_PAGE_LSN
属性。
仍是以上面那张T一、T2事务的图为例。好比写入了mtr_T1_1
后,这个mtr
中的 redo logo 相关的缓存页都会加入 Flush链表中,而后这些缓存页中的FIL_PAGE_LSN
都会更新为 9448
。在写入了 mtr_T1_2
后,相关的缓存页中的FIL_PAGE_LSN
都会更新为10542
。
回到开头的问题,刷入磁盘中的哪部分redo log
能够被覆盖呢?
redo log 只是为了系统崩溃后恢复脏页用的,若是对应的脏页已经刷新到了磁盘,那么就算崩溃后也用不着这部分 redo log 了,那么它占用的磁盘空间就能够被覆盖重用。若是脏页没有刷入磁盘,那么对应的 redo log 就必须保留着。
InnoDB 设计了一个全局变量 checkpoint_lsn
来表明当前系统中能够被覆盖的redo log
总量是多少,这个变量初始值也是8704
。当脏页被刷入磁盘时,就会作一次 checkpoint
来计算 checkpoint_lsn
的值,并写入 redo log 文件中。
作 checkpoint 主要有两个步骤:
脏页只要已经刷入磁盘,那他们对应的redo log就能够被覆盖,那如何判断哪些脏页已经刷入磁盘呢?
前面说过 Flush链表
中的脏页是按修改时间,也就是oldest_modification
表明的LSN值排序的,链表尾部的脏页就是最先修改的,它所对应的oldest_modification
就是最小的一个LSN值,那这个LSN以前的脏页就是已经刷入磁盘的。
在作 checkpoint
时,其实就是将Flush链表尾部的脏页的oldest_modification
赋值给checkpoint_lsn
。
接着根据checkpoint_lsn
计算对应的redo log文件日志偏移量checkpoint_offset
。
InnoDB还设计了一个全局变量checkpoint_no
,表明checkpoint的次数,每作一次checkpoint,这个值就会加1
。
而后就会将这些信息写入日志文件组中的第一个日志文件的checkpoint
中。至于存到 checkpoint1
仍是 checkpoint2
,则根据checkpoint_no
来计算,若是是偶数
,就写到checkpoint1
,若是是奇数
,就写入checkpoint2
。
能够看到checkpoint
中就有三个属性来存储这些信息:
checkpoint_no
写入 LOG_CHECKPOINT_NO
checkpoint_lsn
写入 LOG_CHECKPOINT_LSN
checkpoint_offset
写入 LOG_CHECKPOINT_OFFSET
可使用 SHOW ENGINE INNODB STATUS;
命令查看当前InnoDB存储引擎中的各类LSN值的状况。
---
LOG
---
Log sequence number 294669958009
Log flushed up to 294669958009
Pages flushed up to 294669957358
Last checkpoint at 294669957349
0 pending log flushes, 0 pending chkp writes
21957055 log i/o's done, 1.98 log i/o's/second
复制代码
其中的信息以下:
Log sequence number
:表明系统中的LSN
值,也就是当前系统已经写入的redo log总量。
Log flushed up to
:表明当前系统已经写入磁盘的redo log量。
Pages flushed up to
:表明Flush链表
尾部最先被修改的那个页面对应的oldest_modification
属性值。
Last checkpoint at
:当前系统的checkpoint_lsn
值。
例如上面的信息中,Log sequence number
和 Log flushed up to
相等,说明 redo log buffer 中的redo log 都已经刷到 redo log 文件了。可是 Last checkpoint at
小于 Log sequence number
,说明还有一部分脏页在Flush链表
中没有刷到磁盘。
InnoDB在启动时无论上次数据库是否正常关闭,都会尝试进行恢复操做。若是数据库是正常关闭,redo log 其实没什么用,但若是数据库宕机,redo log 就能够用来恢复数据了。
恢复的起点
首先要读取日志组中的第一个 redo log 文件头部的两个 checkpoint,先比较其中的 checkpoint_no
,哪一个大就使用哪一个 checkpoint。
而后读取 checkpoint_lsn
,这个值以前的都是已经刷盘了的,但以后的可能刷盘了,也可能没有刷盘。因此恢复的起点就是 checkpoint_lsn
对应的文件偏移量,从这个偏移量开始读取 redo log 来恢复页面。
恢复的终点
redo log block
的头部header中有一个属性 LOG_BLOCK_HDR_DATA_LEN
记录了当前block里使用了多少字节的空间,对于被写满的block来讲,该属性就是512
。若是该属性的值不为512,说明这个block还没写满,那终点就是这个block了。
使用哈希表
读取到内存中的 redo log,并非直接就按顺序去重作页的。而是使用了一个哈希表来加快恢复的速度。
它会根据 redo log 的表空间ID
和页号
计算出散列值,以此做为哈希表的 Key,哈希表的 Value 则是一个链表,相同表空间ID和页号的 redo log 就会挨个按顺序加入这个链表中。
以后就遍历哈希表来恢复页,由于对同一个页面修改的 redo log 都在一个链表中,因此能够一次性将一个页面修复好(避免了不少读取页面的随机IO),这样能够加快恢复速度。
跳过已经刷新到磁盘的页面
checkpoint_lsn
以前的能够保证 redo log 对应的脏页已经刷盘了,可是以后的就不能肯定了。由于在作 checkpoint
以后,可能一些脏页会不断的被刷到磁盘中,那这部分 redo log 就不能在页中重作一遍。
这个时候就会用到前面说过的页中的FIL_PAGE_LSN
属性,这个属性记录了最近一次修改页面对应的LSN
值。
若是在作了某次checkpoint
以后有脏页被刷新到磁盘中,那么该页对应的FIL_PAGE_LSN
表明的LSN
值确定大于checkpoint_lsn
的值,对于这种页面就不须要在应用 redo log 了。