MySQL5.5 版本开始,默认使用InnoDB存储引擎,它擅长事务处理,具有崩溃恢复特性,在日常开发中使用非常广泛。下面是InnoDB架构图,左侧为内存结构,右侧为磁盘结构。

磁盘结构
接下来,再来看看InnoDB体系结构的右边部分,也就是磁盘结构:
System Tablespace
系统表空间是Change Buffer的存储区域。如果表是在系统表空间而不是每个表文件或通用表空间中创建的,它也可能包含表和索引数据。(在MySQL5.x版本中还包含InnoDB数据字典、undolog等)
参数:innodb_data_file_path
File-Per-Table Tablespaces
如果开启了innodb_file_per_table开关 ,则每个表的文件表空间包含单个InnoDB表的数据和索引 ,并存储在文件系统上的单个数据文件中。
开关参数:innodb_file_per_table,该参数默认开启。那也就是说,我们每创建一个表,都会产生一个表空间文件。
优点:
- 在File-Per-Table表空间下,删除或者清空表后,存储空间会立刻返回给操作系统。而在共享表空间下,表空间数据文件的大小不会缩小。
- 对共享表空间下的表进行修改时,会增加表空间的磁盘空间。该空间不会像File-Per-Table表空间那样释放回操作系统。
- 清空表操作性能更好
- File-Per-Table 表空间数据文件可以创建在不同的存储设备上,对于IO优化,空间管理或备份操作更加方便
- 可以通过复制File-Per-Table表空间的对应表的数据文件到其他Mysql数据库实例的表空间下实现表的导入迁移
- File-Per-Table表空间中创建的表支持Dynamic和压缩行格式
- File-Per-Table表空间可以节省故障恢复时间,提高数据损坏恢复的成功几率
- 更快速的备份机制,不用中断其他表正在使用的InnoDB表
- 能够通过监视文件大小来监视表的大小
- 常见的Linux系统不允许并发写入单个文件。File-Per-Table表空间提高了并发读写的性能
- 共享表空间中表的大小受到64TB表空间大小限制
缺点:
- 可能存在空间浪费
- fsync操作在每个表的多个数据文件上执行,而不是在单个共享上执行表空间数据文件
- mysqld进行必须为每个表文件表空间保留一个打开的文件句柄,这可能会对性能有一定的影响
- 会产生更多的磁盘碎片
General Tablespaces
通用表空间,需要通过 CREATE TABLESPACE 语法创建通用表空间,在创建表时,可以指定该表空间。
- 创建表空间
CREATE TABLESPACE ts_name ADD DATAFILE 'file_name' ENGINE = engine_name;
- 创建表时指定表空间
CREATE TABLE xxx ... TABLESPACE ts_name;
Undo Tablespaces
回滚表空间,MySQL实例在初始化时会自动创建两个默认的undo表空间(初始大小16M),用于存储 undo log日志。
Temporary Tablespaces
InnoDB使用会话临时表空间和全局临时表空间。存储用户创建的临时表等数据。
Doublewrite Buffer Files
双写缓冲区,InnoDB引擎将数据页从Buffer Pool刷新到磁盘前,先将数据页写入双写缓冲区文件中,便于系统异常时恢复数据。
是什么
Doublewrite Buffer是InnoDB在表空间上的128个页(2 个区),大小是 2MB。为了解决partial page write问题,当MySQL将脏数据刷新到磁盘的时候,先使用memcopy将脏数据复制到内存中的Doublewrite Buffer,之后通过Doublewrite Buffer再分2次,每次写入1MB到共享表空间,然后马上调用fsync函数同步到磁盘上,避免缓冲带来的问题。在这个过程中,Doublewrite是顺序写开销并不大,在完成Doublewrite写入后,再将Doublewrite Buffer写入各表空间文件,这时是离散写入。
所以在正常的情况下,MySQL写数据页时,会写两遍到磁盘上,第一遍是写到Doublewrite Buffer,第二遍是从Doublewrite Buffer写到真正的数据文件中。如果发生了极端情况(断电等),InnoDB再次启动后,发现了一个数据页已经损坏,那么此时就可以从Doublewrite Buffer中进行数据恢复了。

有了
Double write后的脏页刷新流程就是多了几步操作:
- 在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是会通过memcopy函数将脏页先复制到内存中的
Double write Buffer- 通过
Double write Buffer再分两次,每次1MB顺序地写入共享表空间的物理磁盘上,然后马上调用fsync函数,同步磁盘,避免缓冲写带来的问题
缺点是什么
位于共享表空间上的Doublewrite Buffer实际上也是一个文件,写共享表空间会导致系统有更多的fsync操作,而硬盘的fsync性能因素会降低 MySQL 的整体性能,但是并不会降低到原来的 50%。这主要是因为:
Doublewrite是在一个连续的存储空间,所以硬盘在写数据的时候是顺序写,而不是随机写,这样性能更高。- 将数据从
Doublewrite B``uffer写到真正的 segment 中的时候,系统会自动合并连接空间刷新的方式,每次可以刷新多个 pages。
是否一定需要 Double Write
在一些情况下可以关闭Doublewrite以获取更高的性能。比如在slave上可以关闭,因为即使出现了partial page write问题,数据还是可以从中继日志中恢复。设置InnoDB_doublewrite=0即可关闭。
崩溃恢复
如果操作系统在将页写入磁盘的过程中发生了崩溃,在恢复过程中,InnoDB存储引擎可以从共享表空间中的Double write中找到该页的一个副本,将其复制到表空间文件,再应用重做日志。

Redo Log
重做日志是用来实现事务的持久性。该日志文件由两部分组成:重做日志缓冲(Redo Log Buffer)以及重做日志文件(Redo Log),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都会存到该日志中,用于在刷新脏页到磁盘发生错误时,进行数据恢复使用。
以循环方式写入重做日志文件,涉及两个文件:
-rw-r-----. 1 mysql mysql 50331648 09月 12 22:52 ib_logfile0
-rw-r-----. 1 mysql mysql 50331648 09月 12 22:52 ib_logfile1
什么是Mini-Transaction
关于Mini-Transaction,Mysql是这样进行定义的:Innodb引擎对底层页的一次原子访问的过程叫做Mini-Transaction。什么意思呢?我们来举一个例子,假如我们在有索引的数据表中插入一条记录,那么我们需要在Innodb引擎的聚簇索引B+树的数据页中插入这条记录,更改这条记录的上一条记录的next_record的内容,使其指向这条记录;然后还需要在辅助索引B+树的数据页的中插入这条记录的索引信息;上边描述的这个过程就称为对底层页的一次原子访问,也就是说这次原子访问可能修改了多个数据页的信息,这个过程是不可分割的。也就是说我们不能只在聚簇索引B+树的数据页中插入了一条记录,而相应的辅助索引B+树的数据页却没有做变更,我们就把这个不可分割的访问过程称为Mini-Transaction。
我们可能会有疑问Mini-Transaction和Redo Log又有什么关系呢?还是前面这个例子,向有索引的数据表中插入一条记录会产生多条Redo Log,假如这些Redo Log只插入了其中一部分时数据库宕机了。假如我们只恢复了一部分Redo Log,那么这张数据表所对应的B+树就处于不正确的状态了。Innodb为了解决这个问题对一个Mini-Transactoin产生的Redo Log日志都会被划分到一个组当中去,在进行系统崩溃重启恢复时,针对某个组中的Redo Log要么把全部的日志都恢复,要么就一条也不恢复。
为了实现Redo Log日志组的这个功能,Innodb引擎设计了一种特殊的Redo Log日志类型,该类型名称为MLOG_MULTI_REC_END,type字段对应的十进制数字为31,该类型的Redo Log日志结构很简单,只有一个type字段,所以一个Mini-Transaction操作产生的一组Redo Log日志就像这样:
Innodb设计了几十种Redo Log日志类型,这些Redo Log在记录时是否也都需要跟我们前面介绍的MLOG_MULTI_REC_END类型的Redo Log一样需要表示是单独的一个组呢?其实不是的,在Redo Log的通用结构中,type字段占8个bit,使用7个bit来表示Redo Log的日志类型已经足够了,剩下1bit就可以专门表示当前的Redo Log是否是一条单独的日志,这样就就不需要每条简单的Redo Log都跟上一条MLOG_MULTI_REC_END类型的Redo Log一样要划分组信息了。

大家可能会想我们这里提到的
Mini-Transaction和Innodb提供的Transaction有什么样的关系呢?
假如Innodb引擎中的一个Transaction由多条Sql语句组成,每条Sql语句又可以由多个mtr组成,每个mtr又包含一组不可分割的Redo Log日志,所以Transaction、Mini-Transaction、Redo Log之间的关系就是这样一种包含的对应关系:
日志格式

Innodb引擎根据具体在数据页中修改了数据的多少划分了几种不同类型的redo log:
MLOG_1BYTE(type字段对应的十进制数字为1):表示在页面的某个偏移量处写入1个字节的redo日志类型。MLOG_2BYTE(type字段对应的十进制数字为2):表示在页面的某个偏移量处写入2个字节的redo日志类型。MLOG_4BYTE(type字段对应的十进制数字为4):表示在页面的某个偏移量处写入4个字节的redo日志类型。MLOG_8BYTE(type字段对应的十进制数字为8):表示在页面的某个偏移量处写入8个字节的redo日志类型。MLOG_WRITE_STRING(type字段对应的十进制数字为30):表示在页面的某个偏移量处写入一串数据。MLOG_COMP_REC_INSERT(对应的十进制数字为38):表示插入一条使用紧凑行格式的记录时的redo日志类型。MLOG_COMP_PAGE_CREATE(type字段对应的十进制数字为58):表示创建一个存储紧凑行格式记录的页面的redo日志类型。MLOG_COMP_REC_DELETE(type字段对应的十进制数字为42):表示删除一条使用紧凑行格式记录的redo日志类型。MLOG_COMP_LIST_START_DELETE(type字段对应的十进制数字为44):表示从某条给定记录开始删除页面中的一系列使用紧凑行格式记录的redo日志类型。MLOG_COMP_LIST_END_DELETE(type字段对应的十进制数字为43):与MLOG_COMP_LIST_START_DELETE类型的redo日志呼应,表示删除一系列记录直到MLOG_COMP_LIST_END_DELETE类型的redo日志对应的记录为止。
例如LOG_WRITE_STRING类型的日志来,我们在data部分需要记录三种信息:
- 要修改的内容在数据页中的偏移量(offset)
- 修改了多少个字节的数据(len)
- 修改后的数据内容是什么(content)

Log Buffer如何存储redo log
Innodb为了方便业务数据的管理设计了数据页来存放记录。同样的为了更加方便的使用Redo log,也设计了一种结构Redo log`` block , 用来存放Redo log。
Redo log block组成结构
我们将redo log block称为存放redo log的小池子。说它小,是因为相比较于16KB的数据页而言,一个redo log block只有512B,结构也是非常简单的:
| 名称 | 说明 | 长度(字节) | |
|---|---|---|---|
| log block header | LOG_BLOCK_HDR_NO | 每一个block都有一个大于0的唯一标号,表示标号值。 | 4 |
| LOG_BLOCK_HDR_DATA_LENLOG_BLOCK_H | 表示block中已经使用了多少字节,初始值为12(因为log block body从第12个字节处开始)。随着往block中写入的redolog越来也多,值也跟着增长。如果log block body已经被全部写满,那么本属性的值被设置为512。 | 2 | |
| LOG_BLOCK_FIRST_REC_GROUP | 一条redo日志也可以称之为一条redo日志记录(redo log record),一个mtr会生产多条redo日志记录,这些redo日志记录被称之为一个redo日志记录组(redo log record group)。LOG_BLOCK_FIRST_REC_GROUP就代表该block中第一个mtr生成的redo日志记录组的偏移量(其实也就是这个block里第一个mtr生成的第一条redo日志的偏移量)。 | 2 | |
| LOG_BLOCK_CHECKPOINT_NO | 表示所谓的checkpoint的序号。 | 4 | |
| log block body | 真正存储redo log数据的地方 | 496 | |
| log block tailer | LOG_BLOCK_CHECKSUM | 校验数据完整性 | 4 |
Redo log是怎样写入到Log Buffer中的
Innodb的内存架构主要分两部分,Buffer Pool主要是为了解决每次数据页更新都同步磁盘的问题,另一个就是Log Buffer是为了解决Redo log日志直接写磁盘带来的性能损耗问题。在Mysql服务器启动时就向操作系统申请了一大片Redo Log`` Buffer 的连续内存空间,Log Buffer内存空间的大小由启动参数innodb_log_buffer_size来指定,默认是16MB,这片内存空间被划分成若干个连续的Redo Log`` Block 。
Innodb还定义了一个buf_free的全局变量,表示Redo Log日志写到了Log Buffer的哪个位置,如下图所示:

在Mysql5.7版本,向Log Buffer中写入日志是串行的,严格按照Mini-Transaction产生的日志顺序提交的,但是一个事务可能包含多个Mini-Transaction,每个Mini-Transaction都会包含一组Redo log。在Innodb中事务是并发执行的,所以多个事务的Mini-Transaction产生的Redo log也可能是交叉写入到Log Buffer中的。比如我们有两个事务T1和T2,T1会产生T1-MTR1、T1-MTR2、T1-MTR3三组Redo Log,T2会产生T2-MTR1、T2-MTR2两组Redo Log,每一个MTR产生的日志大小不同。如下图所示T1和T2事务并发执行过程中,Redo Log向Log Buffer中写入的日志可能是这样子的:

其中T1-MTR1先写入到Log Buffer,并没有占满一个block,紧接着T2-MTR1也写入了Log Buffer并横跨了三个block,紧接着又提交了MTR,然后T1的剩余MTR才提交,都是一些小的日志,填充到一个block中。Mysql8.0版本实现了MTR日志的并行提交,大体的实现思路就是一个当MTR产生一组Redo Log时,它占用的空间就已经是确定的了,由此就可以计算出每个MTR应该将日志写入到Log Buffer的什么位置,从而可以实现多线程的日志并行复制,但是并行复制有快有慢,由此产生的日志空洞、刷脏顺序等问题还需要额外解决。
磁盘如何存储redo log
InnoDB的Redo Log可以通过参数innodb_log_files_in_group配置成多个文件,另外一个参数innodb_log_file_size表示每个文件的大小。因此总的Redo Log大小为innodb_log_files_in_group * innodb_log_file_size。Redo Log文件以ib_logfile[number]命名,日志目录可以通过参数innodb_log_group_home_dir控制。Redo Log以顺序的方式写入文件文件,写满时则回溯到第一个文件,进行覆盖写。整个过程如下图所示:

Log Buffer本质上是由若干个512字节大小的block组成的一片连续的内存空间,Redo Log落盘也是以block为单位写到日志组文件中去的。所以磁盘上的每一个日志组文件也都是由512字节的block组成的,并且每一个日志组文件的前4个block(2048个字节)都是用来存储管理信息使用的。如下图所示:

log file header的组成结构:
| 属性名 | 长度(单位:字节) | 描述 |
|---|---|---|
| LOG_HEADER_FORMAT | 4 | redo日志的版本 |
| LOG_HEADER_PAD1 | 4 | 做字节填充用的,没什么实际意义 |
| LOG_HEADER_START_LSN | 8 | 标记本redo日志文件开始的LSN值,也就是文件偏移量为2048字节初对应的LSN值 |
| LOG_HEADER_CREATOR | 32 | 一个字符串,标记本redo日志文件的创建者是谁。 |
| LOG_BLOCK_CHECKSUM | 4 | block的校验值,所有block都有 |
checkpoint的组成结构:
| 属性名 | 长度(单位:字节) | 描述 |
|---|---|---|
| LOG_CHECKPOINT_NO | 8 | 服务器做checkpoint的编号,每做一次checkpoint,该值就加1 |
| LOG_CHECKPOINT_LSN | 8 | 服务器做checkpoint结束时对应的LSN值,系统崩溃恢复时将从该值开始 |
| LOG_CHECKPOINT_OFFSET | 8 | 上个属性中的LSN值在redo日志文件组中的偏移量 |
| LOG_CHECKPOINT_LOG_BUF_SIZE | 8 | 服务器在做checkpoint操作时对应的log buffer的大小 |
| LOG_BLOCK_CHECKSUM | 4 | block的校验值 |
那么内存中我们更新的数据,又是如何到磁盘中的呢? 此时就涉及到一组后台线程,下面就介绍InnoDB中涉及到的后台线程。

1611

被折叠的 条评论
为什么被折叠?



