MySQL底层概述—2.InnoDB磁盘结构

大纲

1.InnoDB磁盘结构

2.表空间(Tablespaces)

3.数据字典(Data Dictionary)

4.双写缓冲区(Double Write Buffer Files)

5.重做日志(redo log)

6.撤销日志(undo log)

7.二进制日志(binlog)

8.新版本结构演变

 

1.InnoDB磁盘结构

(1)Tablespaces

(2)Double Write Buffer

(3)redo log

(4)undo log

 

InnoDB磁盘主要包含:

Tablespaces、InnoDB Data Dictionary、Double Write Buffer、redo log和undo log。

 

(1)Tablespaces

表空间分为:系统表空间、临时表空间、常规表空间、Undo表空间及独立表空间。

 

系统表空间又包括:Change Buffer、双写缓冲区(Double Write Buffer)等。

 

(2)Double Write Buffer

InnoDB将数据页写到文件前存放的位置。8.0.20版本之前,Double Write Buffer存放在InnoDB系统表空间中,8.0.20版本后存放在Double Write中。

 

(3)redo log

存储的是Log Buffer刷到磁盘的数据。

 

(4)undo log

存在于全局临时表空间中,用于事务的回滚。

 

2.表空间(Tablespaces)

(1)系统表空间(The System Tablespace)

(2)独立表空间(File-Per-Table Tablespaces)

(3)通用表空间(General Tablespaces)

(4)撤销表空间(Undo Tablespaces)

(5)临时表空间(Temporary Tablespaces)

 

表空间(Tablespaces)是用于存储表结构和数据的,InnoDB表空间类型包括系统表空间、独立表空间、常规表空间、Undo表空间、临时表空间等。

 

(1)系统表空间(The System Tablespace)

系统表空间(又叫共享表空间)包含:InnoDB数据字典、Double Write Buffer、Change Buffer、undo log的存储区域。

 

系统表空间默认包含任何用户在系统表空间创建的表数据和索引数据,系查看统表空间的物理文件如下:

[root@localhost ~]# cd /var/lib/mysql
[root@localhost mysql]# ll ibdata*
-rw-r----- 1 mysql mysql 79691776 1月  25 06:42 ibdata1

系统表空间是一个共享的表空间,它可以被多个表共享。该空间的数据文件通过参数innodb_data_file_path控制,该参数的默认值是ibdata1:12M:autoextend。这个默认值的意思是:文件名为ibdata1、大小为12M、自动扩展。

mysql> show variables like '%innodb_data_file_path%';
+-----------------------+------------------------+
| Variable_name         | Value                  |
+-----------------------+------------------------+
| innodb_data_file_path | ibdata1:12M:autoextend |
+-----------------------+------------------------+
1 row in set (0.38 sec)

(2)独立表空间(File-Per-Table Tablespaces)

一.什么是独立表空间

独立表空间是一个单表表空间,创建一个表就给这个表一个独立表空间。表的数据文件创建和保存于独立表空间中,而不是创建于系统表空间中。

 

默认情况下innodb_file_per_table选项是会开启的。如果innodb_file_per_table开启,表将被创建于独立表空间中;如果innodb_file_per_table关闭,表数据文件将被创建于系统表空间中。

mysql> show variables like '%innodb_file_per_table%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_file_per_table | ON    |
+-----------------------+-------+
1 row in set (0.14 sec)

每个表的独立表空间是一个.ibd后缀的数据文件,该.ibd后缀的数据文件会被创建于数据库目录中。

[root@localhost test1]# cd /var/lib/mysql/test1/
[root@localhost test1]# ll
total 112
-rw-r----- 1 mysql mysql  8582 Dec 27 19:17 a1.frm
-rw-r----- 1 mysql mysql 98304 Dec 27 19:21 a1.ibd
-rw-r----- 1 mysql mysql    67 Dec  9 23:21 db.opt

二.两种表空间的优缺点

系统(共享)表空间的优点:

可以将表空间分成多个文件存放到各个磁盘上。表空间文件大小不受表大小的限制,如一个表可以分布在不同的文件上。数据和文件放在一起方便管理。

 

系统(共享)表空间的缺点:

所有的数据和索引存放到一个文件中,虽然可将一个大文件分成多个小文件,但多个表及索引在表空间中混合存储,这样对一个表进行大量删除操作后表空间中将会有大量的空隙。特别是对于统计分析、日志系统这类应用最不适合用共享表空间。

 

独立表空间的优点:

优点一:每个表都有自己独立的表空间。每个表的数据和索引都会存在自己的表空间中,可以实现单表在不同的数据库中移动。复制独立表空间的数据文件到其他MySQL的表空间下,可实现表的迁移。

优点二:空间可以回收。在独立表空间下,删除或者清空表后,存储空间会立刻返还给操作系统,而在共享表空间下的表空间数据文件的大小不会缩小。

 

独立表空间的缺点:

单表增加过大,如超过100个G。

 

(3)通用表空间(General Tablespaces)

MySQL 5.7开始支持通用表空间管理功能,通用表空间和系统表空间类似,也是共享的表空间,一个通用表空间文件能够存储多个表的数据。

 

通用表空间为通过create tablespace语法创建的共享表空间。通用表空间可以创建于MySQL数据目录外的其他表空间。通用表空间可容纳多张表,且支持所有行格式。

 

相比独立表空间,通用表空间由于多表共享表空间,所以内存消耗会更少,占用的磁盘空间也会更小。

 

下面实现指定存储路径创建一张表:

# 1.创建表空间目录
[root@localhost ~]# mkdir -p /mdata/general
[root@localhost ~]# cd /mdata/general/

# 设置文件所有者为mysql(第一个mysql是用户, 第二个为用户组)
[root@localhost general]# chown mysql.mysql /mdata/general/

# 2.连接MySQL,创建数据库test1的一张表test_ger,并且将该表存放于刚刚创建的通用表空间目录/mdata/general下
mysql> create table test1.test_ger(a int) data directory='/mdata/general';
Query OK, 0 rows affected (0.50 sec)

# 3.查看表空间/mdata/general目录
[root@localhost general]# tree
.
└── test1
  └── test_ger.ibd
  
1 directory, 1 file

# 4.查看数据库test1目录
[root@localhost general]# cd /var/lib/mysql/test1/
[root@localhost test1]# ll
-rw-r----- 1 mysql mysql  8554 1月  29 01:41 test_ger.frm
-rw-r----- 1 mysql mysql    33 1月  29 01:41 test_ger.isl

# 5.cat test_ger.isl 一个文本文件,内容就是idb文件的路径,做了一个链接
[root@localhost test1]# cat test_ger.isl
/mdata/general/test1/test_ger.ibd

# 6.接下来使用通用表空间
# 7.创建一个名为ger_space的通用表空间(file_block_size就是指定page_size大小),指定存放在文件/mdata/general/ger_space.ibd中,并且指定该通用表空间的数据页大小是16KB
mysql> create tablespace ger_space add datafile '/mdata/general/ger_space.ibd' file_block_size=16384;
Query OK, 0 rows affected (0.15 sec)

# 8.查看目录
[root@localhost mysql]# cd /mdata/general/
[root@localhost general]# ll ger*
-rw-r----- 1 mysql mysql 65536 1月  29 01:56 ger_space.ibd

# 9.查看通用表空间信息
mysql> select * from information_schema.innodb_sys_tablespaces where name = 'ger_space'\G;
*************************** 1. row ***************************
       SPACE: 153
        NAME: ger_space
        FLAG: 2048
 FILE_FORMAT: Any
  ROW_FORMAT: Any
   PAGE_SIZE: 16384 --页大小16K
ZIP_PAGE_SIZE: 0
  SPACE_TYPE: General
FS_BLOCK_SIZE: 4096
   FILE_SIZE: 65536
ALLOCATED_SIZE: 32768
1 row in set (0.00 sec)

# 10.接下来在通用表空间中创建数据库test1的一张表test_ger2,这时test_ger2的数据就会放在ger_space1.ibd文件里
mysql> create table test1.test_ger2(a int) tablespace=ger_space;
Query OK, 0 rows affected (0.13 sec)

# 11.查看表信息
mysql> show create table test_ger2;
+-----------+----------------------------------------------------------------------------------------------------------------------------------+
| Table     | Create Table                                                                                                                     |
+-----------+----------------------------------------------------------------------------------------------------------------------------------+
| test_ger2 | CREATE TABLE `test_ger2` (
`a` int(11) DEFAULT NULL
) /*!50100 TABLESPACE `ger_space` */ ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 |

# 12.接下来在通用表空间中创建数据库test1的一张表test_ger3,这时test_ger3的数据也会放在ger_space1.ibd文件里
mysql> create table test1.test_ger3(id int primary key) tablespace=ger_space1;
Query OK, 0 rows affected (0.02 sec)

(4)撤销表空间(Undo Tablespaces)

一.什么是撤销表空间

撤销表空间用来保存回滚日志,即undo log。在MySQL 5.7.0前,undo log的数据是存储在系统表空间ibdata1文件中。在MySQL 5.7.0后,可将undo log数据使用独立的撤销表空间进行存储。undo log的数据默认是存储在系统表空间ibdata1文件中。

 

撤销表空间由一个或多个Undo日志文件组成,它会在对应表空间目录下生成undo_001和undo_002两个文件。撤销表空间的文件必须以".ibu"作为扩展后缀名,撤销表空间也叫回滚表空间。

 

可通过innodb_undo_directory属性查看撤销表空间的位置,撤销表空间的默认存储路径就是MySQL的数据存储路径。

mysql> show variables like 'innodb_undo_directory';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_undo_directory | ./    |
+-----------------------+-------+

InnoDB使用的撤销表空间由innodb_undo_tablespaces配置选项控制。innodb_undo_tablespaces选项可设置撤销表空间个数,范围是0-128。该选项默认为0表示不开启独立的撤销表空间,使用系统表空间ibdata1。该选项大于0则表示存储在撤销表空间undo_001、undo_002文件等。

 

二.什么时候需要开启使用撤销表空间

当DB写压力较大时:

可以设置独立撤销表空间,把undo log从ibdata文件中分离开来,以及设置innodb_undo_directory目录,指定日志存放到高速磁盘上,从而加快undo log的读写性能。

 

在写多读少的场景下:

undo log日志会产生很频繁,系统表空间的ibdata1文件也会越来越大。因为ibdata1文件不能主动释放空间,所以会越来越大,影响读写性能。

 

undo log的数据存放在系统表空间ibdata1文件中是不太合理的,此时可设置参数innodb_undo_log_truncate为开启(默认关闭),以及设置innodb_undo_tablespaces为2。那么undo log的数据就会从ibdata1文件中移出,存储到独立的撤销表空间。

mysql> show variables like '%innodb_undo_tablespace%';
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| innodb_undo_tablespaces | 0     |
+-------------------------+-------+
1 row in set (0.01 sec)

三.undo日志使用共享表空间存在的问题

由于共享表空间不会自动收缩,事务关闭,undo log也一直占用空间。所以可能会出现因为大事务而导致ibdata1文件过大的问题。

 

MySQL5.7中引入了一个新的参数innodb_undo_log_truncate,表示是否开启自动收缩undo log的表空间的操作。如果配置为ON,并且配置了2个或2个以上的undo log表空间数据文件。当某日志文件大小超过设置的最大值后,就会自动收缩表空间数据文件。

 

在回收表空间数据文件时,被回收的表空间数据文件会临时下线。为保证undo log一直可写,此时要保证至少有1个undolog日志文件在线。这就是要求"innodb_undo_tablespaces >= 2"的原因。

 

四.8.0对于undo log存储的进一步优化

从MySQL8.0版本开始,MySQL默认对undo数据进行了分离操作。也就是说,不需要在初始化中手动配置参数。默认会在datadir目录下生成两个undo表空间文件undo_001和undo002,并且可以在线的增加和删除undo表空间文件,进行动态扩容和收缩。

# 查询undo log信息
mysql> select tablespace_name, file_name from information_schema.files where file_type like 'undo log';
+-----------------+------------+
| TABLESPACE_NAME | FILE_NAME  |
+-----------------+------------+
| innodb_undo_001 | ./undo_001 |
| innodb_undo_002 | ./undo_002 |
+-----------------+------------+

# 文件位置 /var/lib/mysql
[root@localhost mysql]# ll undo*
-rw-r-----. 1 mysql mysql 16777216 12月 31 00:21 undo_001
-rw-r-----. 1 mysql mysql 16777216 12月 31 00:21 undo_002

(5)临时表空间(Temporary Tablespaces)

临时表空间用来保存用户创建的临时表和磁盘内部的临时表。在MySQL 5.7.0前,用户创建的临时表和磁盘内部的临时表默认存储在系统表空间ibdata1中。在MySQL 5.7.0后,会将临时表使用独立的临时表空间进行存储,命名ibtmp1文件。临时表空间命名ibtmp1文件,初始化12M,且默认无上限。

 

全局临时表空间默认是数据目录的ibtmp1文件,所有临时表共享。可以通过innodb_temp_data_file_path属性指定临时表空间的位置。

mysql> select @@innodb_temp_data_file_path;
+------------------------------+
| @@innodb_temp_data_file_path |
+------------------------------+
| ibtmp1:12M:autoextend        |
+------------------------------+
1 row in set (0.00 sec)

需要注意的是:临时表空间最好是设置最大增长限制,否则可能会导致ibtmp1文件过大,占用过多的磁盘空间。

 

3.数据字典(Data Dictionary)

数据字典由内部系统表组成。这些表存储了用于查找表、索引和表字段等对象的元数据,即数据字典存储了表结构、数据库名、表名、字段类型等元数据信息,元数据信息存储在InnoDB系统表空间中。

 

MySQL8.0前:

数据字典元数据一定程度上与表的元数据文件(.frm文件)存储的信息重叠,数据字典元数据的存储比较分散,且使用MyISAM存储引擎,管理混乱。

 

MySQL8.0后:

将所有原来存放于数据字典文件中的信息,存放到数据库系统表中。即把.frm、.opt等文件都移除,不再通过文件的方式存储数据字典信息,这时MySQL8.0只剩下.ibd文件。这样做的好处就是:元数据使用InnoDB引擎来存储,从而支持了事务,从而可以原子DDL。

 

4.双写缓冲区(Double Write Buffer Files)

(1)什么是写失效(部分页失效)

(2)双写缓冲区Double Write Buffer

(3)数据双写流程

(4)为什么写两次

 

(1)什么是写失效(部分页失效)

InnoDB的页和操作系统的页大小不一致。InnoDB页大小一般为16K,操作系统页大小为4K。InnoDB的页写入到磁盘时,一个页需要分4次写。

 

如果存储引擎正在把页的数据写入到磁盘时发生了宕机,那么就可能出现页的数据只写了一部分的情况,比如只写了4K就宕机了。这种情况叫做部分写失效(Partial Page Write),可能会导致数据丢失。

 

有人可能会想到,如果发生写失效,MySQL可以根据redo log进行恢复。但是redo log中记录的只是对页的物理修改,如偏移量800写'abc'记录。如果这个脏页本身已经损坏,再对其进行重做redo log是没有用的。可以理解为脏页写一半时机器宕机,会破坏其对应的数据页的完整性。当然其他还没写的脏页,自然不会被破坏,它们可以通过重做日志恢复。注意:数据页有checksum机制检查是否完整。

(2)双写缓冲区Double Write Buffer

为了解决部分写失效问题,InnoDB使用了Double Write Buffer。Double Write Buffer位于系统表空间中,是一个存储区域。将Buffer Pool的脏页刷入磁盘前,会先将数据存在Double Write缓冲区。

 

这样在宕机重启时发现数据页损坏(如脏页断裂),则在应用redo log前:首先通过Double Write中该页的副本来还原该页,然后再redo log重做。这样通过Double Write就实现了InnoDB数据页的可靠性。

 

默认情况下会启用双写缓冲区,如果要禁用Double Write缓冲区,可将innodb_doublewrite设为0。

mysql> show variables like '%innodb_doublewrite%';
+--------------------+-------+
| Variable_name      | Value |
+--------------------+-------+
| innodb_doublewrite | ON    |
+--------------------+-------+
1 row in set (0.01 sec)

(3)数据双写流程

第一步:当把缓冲池中的脏页刷新到磁盘时,并不会直接写磁盘,每次脏页刷新必须要先写Double Write Buffer。

 

第二步:通过memcpy系统调用将脏页复制到内存中的Double Write Buffer,内存中的Double Write Buffer是2M的连续内存。

 

第三步:将Double Write Buffer中的脏页顺序写入共享表空间的物理磁盘上,此时是第一次写。

 

第四步:在完成Double Write Buffer中脏页的顺序写入后,再将Double Wirite Buffer中的脏页写入各个表的独立表空间文件中。此时是第二次写,也就是所谓的将脏页刷入磁盘。

 

(4)为什么写两次

为什么写两次,为什么还要写共享表空间?首先第一次写是顺序写入共享表空间,第二次写是随机写入独立表空间。共享表空间中会划出2M连续的空间,专门给Double Write刷脏页。由于共享表空间存储脏页是连续的,因此第一次写入为顺序写,性能高。完成第一次写之后,再将脏页写入各个表空间文件,此时写入是随机写。这样即便第二次随机写发生页断裂,也能从第一次顺序写的空间中恢复。

 

(5)Change Buffer

系统表空间中的Change Buffer是内存中Change Buffer的备份,内存中Change Buffer也会被持久化到系统表空间中,在崩溃恢复时会从系统表空间的Change Buffer读取信息到Buffer Pool。

 

5.重做日志(redo log)

(1)WAL(Write-Ahead Logging)机制

(2)为什么使用WAL

(3)redo log基本概念

(4)redo log数据落盘的机制

(5)redo log日志格式

(6)redo log日志类型

(7)redo log写入机制

 

(1)WAL(Write-Ahead Logging)机制

WAL全称Write-Ahead Logging,写前日志,是数据安全的写入机制。就是先写日志再写磁盘,这样既能提高性能又可以保证数据的安全性。InnoDB中的redo log就是采用WAL机制来实现的。

 

(2)为什么使用WAL

磁盘的写操作是随机IO,比较耗性能,所以如果把每一次的更新操作都先写入log中,那么就成了顺序写操作。实际更新操作由后台线程再根据log异步写入,这样可降低客户端延迟。而且由于顺序写大概率在一个磁盘块内,这样产生的IO次数也大大降低。所以WAL的核心就是将随机写转变为了顺序写,WAL可以降低进行磁盘写操作时的客户端延迟,以及提升吞吐量。

 

WAL的好处总结:

一.保证数据的安全性

二.将随机写变成了顺序写

 

(3)redo log基本概念

InnoDB引擎对数据的更新,会首先将更新记录写入redo log日志,然后在系统空闲时或按设定的更新策略再将日志中的内容更新到磁盘中。这就是所谓的WAL预写式技术(Write Ahead Logging),这种技术可以大大减少IO操作的频率,提升数据刷新的效率。

 

redo log被称作重做日志,包括两部分:内存中的日志缓冲Redo Log Buffer + 磁盘上的日志文件Redo Log File,可以通过redo log来实现事务的持久性。

 

InnoDB每执行一条DML语句,都会先将记录写入Redo Log Buffer,后续某个时间点再一次性将多个操作记录写到Redo Log File。当故障发生致使内存数据丢失后,InnoDB会在重启时:通过重放redo log,就可以将数据页恢复到崩溃前的状态(实现事务的持久性)。

 

(4)redo log数据落盘的机制

将内存中的缓存页持久化到磁盘,需要两个流程来完成:脏页落盘 + redo log持久化。

 

一.脏页落盘

脏页是指在Buffer Pool被修改的缓存页,当Buffer Pool中的缓存页和磁盘中的数据页不一致,便出现了脏页。

 

当进行数据页的修改操作时:首先会修改在Buffer Pool中的页,然后再以一定的频率刷新到磁盘上。每当更新缓存页时,并不会触发缓存页从Buffer Pool刷新回磁盘的操作。更新的缓存页会通过一种叫CheckPoint的机制来刷新回磁盘。

什么是CheckPoint机制?

思考一下这个场景:如果重做日志可以无限增大,同时缓冲池也足够大。那么这时是不需要将缓冲池中的脏页刷新回磁盘的,因为当发生宕机时,完全可通过重做日志来恢复系统宕机时的数据。

 

所以CheckPoint技术主要解决以下几个问题:

问题一:缩短数据库的恢复时间

问题二:缓冲池不够用时,将脏页刷盘

问题三:重做日志不可用时,刷新脏页

 

脏页落盘的时机:采用CheckPoint检查点机制

以下机制都可以通过参数控制:

Sharp CheckPoint:强制落盘,把内存中所有的脏页都执行落盘操作,只有当关闭数据库之前才会执行;
Fuzzy CheckPoint:模糊落盘,把一部分脏页执行落盘操作
  1.Master Thrad CheckPoint,主线程定时将脏页写入磁盘,每秒或每10s执行一次脏页;
  2.FLUSH_LRU_LIST CheckPoint,Buffer Pool中的LRU链表有脏页换出,执行落盘
  3.Async/Sync Flush CheckPoint,当redo log快写满时执行落盘
    a.当redo log超过75%小于90%会执行异步落盘
    b.当redo log超过90%,会执行同步落盘操作,会阻塞写操作
  4.Dirty Page too much CheckPoint,如果Buffer Pool中脏页太多,脏页率超过75%执行落盘

二.redo log的持久化

缓冲区数据一般情况下是无法直接写入磁盘的,中间必须经过操作系统缓冲区(OS Cache)。因此Redo Log Buffer写入redo log文件实际上是先写入OS Cache,然后再通过系统调用fsync()函数将其刷到redo log文件。

 

Redo Log Buffer持久化到redo log文件的策略,可通过Innodb_flush_log_at_trx_commit设置。

 

策略一:Innodb_flush_log_at_trx_commit = 0(延迟写,延迟刷)

事务提交时不会将Redo Log Buffer中的日志写入到OS Cache,而是每秒写入OS Cache并调用fsync()写入到redo log文件中。设置为0时是大约每秒刷新写入到磁盘中的,所以当MySQL崩溃,会丢失1秒钟的数据。

 

策略二:Innodb_flush_log_at_trx_commit = 1(实时写,实时刷)

每次提交事务都会将Redo Log Buffer中的日志写入OS Cache,并且同时会调用fsync()将OS Cache的日志写入redo log文件中。这种方式即使系统崩溃也不会丢失任何数据,但是因为每次提交事务都写入磁盘,IO性能较差。

 

策略三:Innodb_flush_log_at_trx_commit = 2(实时写,延迟刷)

每次提交事务都会将Redo Log Buffer中的日志写入OS Cache,然后每秒调用fsync()将OS Cache中的日志写入redo log文件中。

 

一般选策略三,因为MySQL挂了数据不丢失,系统挂了才丢1秒的数据。

(5)redo log日志格式

物理日志VS逻辑日志:

一.物理日志

记录每一个数据页具体存储的值是多少,在这个数据页上做了什么修改。比如某事务将系统表空间的第1个页中偏移量为1的那个字节的值改为2。

 

二.逻辑日志

记录的是每一个数据页面中具体数据是如何变动的。逻辑日志会记录一个变动的过程或SQL语句的逻辑。比如把一个数据页中的某条数据从1改为2,再从2改为3。逻辑日志就会记录1 -> 2,2 -> 3这个数据变化的过程。redo日志属于物理日志,只是记录一下事务对数据库做了哪些修改。

一.Type:该条日志的类型

二.Space ID:表空间ID

三.Page Number:数据页号

四.Data:该条redo日志的具体内容

 

(6)redo log日志类型

redo log根据在页中写入数据的多少,将redo日志分为几种不同的类型:

一.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)

表示在页面的某个偏移量处写入一串数据,但是因为不能确定写入的具体数据占用多少字节,所以需要在日志结构中添加一个len字段。

(7)redo log写入机制

Redo Log Buffer是用来缓存写入到redo log文件中的数据内容的,但不是每次Redo Log Buffer产生内容就立即写入磁盘进行持久化的。

 

在事务执行期间,即使MySQL宕机 + Redo Log Buffer中的内容丢失,也不会有损失。因为事务并没有提交,事务提交必然已完成redo log日志的写入。

 

一.redo log三种状态

状态1:存在于Redo Log Buffer内存区域中

状态2:向磁盘写入,但没真正写入磁盘,而是保存在OS Cache中

状态3:已经持久化到磁盘

二.触发redo log真正fsync写盘的场景

如果事务没提交,Redo Log Buffer的部分日志有可能被持久化到磁盘。

 

场景一:Redo Log Buffer占用空间达到innodb_log_buffer_size一半时,此时后台线程会主动将Redo Log Buffer中的日志写入磁盘。

 

场景二:有并行事务提交时,可能会顺带将某个未提交的事务的Redo Log Buffer持久化到磁盘。因为Redo Log Buffer是共享的,因此一些正在执行的事务的redo log也有可能被持久化到磁盘中。

 

三.组提交

MySQL为优化磁盘持久化的开销,会有个组提交(Group Commit)机制。每个InnDB存储引擎至少有1个重做日志文件组(Group)。每个文件组下至少有两个重做日志文件,默认为ib_logfile0、ib_logfile1。

 

如下查看重做日志文件组路径,其中./表示当前目录,即MyQSL数据目录就是重做日志文件组目录:

mysql> show variables like 'innodb_log_group_home_dir';
+---------------------------+-------+
| Variable_name             | Value |
+---------------------------+-------+
| innodb_log_group_home_dir | ./    |
+---------------------------+-------+
1 row in set (0.00 sec)
[root@localhost mysql]# ll ib_log*
-rw-r----- 1 mysql mysql 50331648 1月  29 03:39 ib_logfile0
-rw-r----- 1 mysql mysql 50331648 7月  11 2020 ib_logfile1

如下是查看重做日志文件组中的重做日志文件个数,默认是2个:

mysql> show variables like 'innodb_log_files_in_group';
+---------------------------+-------+
| Variable_name             | Value |
+---------------------------+-------+
| innodb_log_files_in_group | 2     |
+---------------------------+-------+
1 row in set (0.01 sec)

如下是查看重做日志文件大小,日志组中每个重做日志文件的大小是一致的,都是48M,并循环使用。

mysql> show variables like 'innodb_log_file_size';
+----------------------+----------+
| Variable_name        | Value    |
+----------------------+----------+
| innodb_log_file_size | 50331648 |
+----------------------+----------+
1 row in set (0.00 sec)

InnoDB以环型方式(Circular Fashion)将数据写入到重做日志文件。当日志文件1满时,会自动切换到日志文件2;当日志文件2满时,会切换到日志文件1。

WritePos指针:表示日志当前记录的位置,当ib_logfile_4写满后,会从ib_logfile_1从头开始记录。

 

CheckPoint指针:表示将日志记录的修改已写进磁盘,已完成数据落盘。数据落盘后CheckPoint会将日志上的相关记录擦除掉。

 

正常情况下,WritePos是大于或早于CheckPoint的。WritePos -> CheckPoint之间是redo log空闲部分,用于记录新记录;CheckPoint -> WritePos之间是redo log待落盘的数据修改记录。

 

如果WritePos追上CheckPoint,表示写满,此时不能再执行新的更新。需要先停下来擦掉一些记录,把CheckPoint推进一下。

 

6.撤销日志(undo log)

(1)undo log基本概念

(2)undo log的作用

(3)undo log的工作原理

(4)undo log的存储机制

(5)undo log相关参数

 

(1)undo log基本概念

undo log是一种用于撤销回退的日志。在数据库事务开始前,会先记录更新前的数据到undo log日志文件里。在事务回滚时或者数据库崩溃时,可以利用undo log来进行回退。

 

undo log产生和销毁:

事务开始前InnoDB会产生undo log,事务提交时InnoDB并不会立刻删除undo log,而是会将该事务对应的undo log放入到删除列表中,后面会通过后台线程Purge Thread进行回收处理。

 

注意:undo log也会产生redo log,因为undo log也要实现持久性保护。

 

(2)undo log的作用

一.提供回滚操作(undo log实现事务的原子性)

在数据修改时,不仅记录了redo log,还记录了相对应的undo log。如果因某些原因导致事务失败或回滚了,可借助undo log进行回滚。

 

与redo log记录物理日志不一样,undo log记录的是逻辑日志。当add一条记录时,undo log中会记录一条对应的delete记录。当delete一条记录时,undo log中会记录一条对应的insert记录。当update一条记录时,undo log中会记录一条对应相反的update记录。

 

undo log记录的是事务开始前的数据状态,记录的是更新前的值。所以undo log可以提供回滚,实现事务的原子性。

 

redo log记录的是事务完成后的数据状态,记录的是更新后的值。所以redo log可以崩溃恢复,实现事务的持久性。

 

二.提供多版本控制(MVCC)(undo log实现多版本并发控制MVCC)

MVCC,即多版本控制。在InnoDB存储引擎中,会用undo Log来实现多版本并发控制(MVCC)。当读取的某一行记录被其他事务锁定时,可从undo log中分析出该行记录以前的数据版本,从而能够让用户读取到当前事务操作前的数据(快照读)。

 

快照读:SQL读取的数据是快照版本(可见版本),也就是历史版本。快照读不用加锁,普通的select就是快照读。

 

当前读:SQL读取的数据是最新版本(最新版本指的是修改且已提交的数据),在执行update语句时,需要进行当前读。除了更新数据外,select语句也有可能是当前读。比如"select ... lock in share mode"、"select ... for update"就是当前读。

 

(3)undo log的工作原理

在更新数据前,InnoDB会提前生成undo log日志。当事务提交时,并不会立即删除undo log,因后面可能要进行回滚操作。undo log日志的删除是通过通过后台Purge线程进行回收处理的。

 

如下图示,事务A执行update更新操作:在事务没有提交前,会将旧版本数据备份到对应的Undo Log Buffer中。然后再从Undo Log Buffer持久化到磁盘中的undo log文件中,之后才会对user的缓存页进行更新操作,最后持久化到磁盘。

 

在事务A执行的过程中,事务B的过程中可能会对user进行查询:此时会从内存进行快照读,读取旧版本数据。

(4)undo log的存储机制

为了保证事务并发操作时,在写各自的undo log时不产生冲突,InnoDB采用回滚段的方式来维护undo log的并发写入和持久化。

 

Rollback Segment称为回滚段,共有128个回滚段。每个回滚段中有1024个Undo Log Segment,所以回滚段可以支持128 * 1024个undo操作,也就是回滚段最多可以支持(128 * 1024)约13万个并发事务执行。

 

接下来看一条insert语句的undo log的内容:

 

一.事务ID

事务执行过程中在对某个表执行增删改操作时,InnoDB就会给这个事务分配一个唯一的事务ID。information_schema.INNODB_TRX会存储当前系统中运行的事务信息。

START TRANSACTION;
    select * from test1.a1;
    commit;
-- trx_id 就是事务ID,InnoDB在内存维护了一个全局变量来表示事务ID
-- 每当要分配一个事务ID时,就获取这个变量值,然后把这个变量自增1
select 
    trx_id,
    trx_state,
    trx_started,
    trx_isolation_level
from information_schema.INNODB_TRX;

二.行记录的隐藏列

InnoDB引擎管理的数据表中每行行记录,都存在着三个隐藏列。

 

DB_ROW_ID:

如果没有为表显式的定义主键,并且表中也没有定义唯一索引,那么InnoDB会自动为表添加一个row_id的隐藏列作为主键。

 

DB_TRX_ID:

某事务对某条记录增删改时,就会将这个事务的ID写入行记录trx_id中。

 

DB_ROLL_PTR:

回滚指针,指向undo log的指针。

三.INSERT语句的undo log回滚日志结构

INSERT语句对应的undo操作就是根据主键删除这条数据,所以insert对应的undo log只要把这条记录的主键进行记录下来即可。

start、end:指向记录开始和结束的位置;
主键列信息:记录insert这行数据的主键ID信息,或者唯一列信息;
table id:表空间ID(表ID);
undo no:在当前事务中undo log的编号(日志编号),从0开始;
undo type:undo log的类型(日志类型);
insert语句的undo log类型是TRX_UNDO_INSERT_REC;

四.undo回滚链

通过一个事务操作来看一下回滚链的形成:

(5)undo log相关参数

一.MySQL与undo相关的参数设置

mysql> show variables like '%undo%';
+--------------------------+------------+
| Variable_name            | Value      |
+--------------------------+------------+
| innodb_max_undo_log_size | 1073741824 |
| innodb_undo_directory    | ./         |
| innodb_undo_log_truncate | OFF        |
| innodb_undo_logs         | 128        |
| innodb_undo_tablespaces  | 0          |
+--------------------------+------------+


mysql> show global variables like '%truncate%';
+--------------------------------------+-------+
| Variable_name                        | Value |
+--------------------------------------+-------+
| innodb_purge_rseg_truncate_frequency | 128   |
| innodb_undo_log_truncate             | OFF   |
+--------------------------------------+-------+

二.innodb_max_undo_log_size

表示每一个undo log对应的日志文件的最大值,默认最大值为1G,默认初始化大小为10M。当日志文件达到该阈值后,且参数innodb_undo_log_truncate=ON时,才会触发truncate回收动作,被truncate后的表空间文件大小,会缩小到undo log表空间数据文件默认的10M大小,否则即便是到达最大值后,也不会自动回收undo log的表空间。

 

三.innodb_undo_directory

指定undo log日志的存储目录,默认值为./。

 

四.innodb_undo_logs

在MySQL5.6版本后,可以通过此参数自定义多少个Rollback Segment,默认值为128。

 

五.innodb_undo_tablespaces

设置undo独立表空间个数,范围为0-128, 默认为0。默认0表示不开启独立undo表空间,而且undo日志会存储在ibdata文件中,即系统表空间的5号。

 

什么时候需要设置innodb_undo_tablespaces参数?当DB写压力较大时,可以设置独立undo表空间,把undo从ibdata文件中分离开来,指定innodb_undo_directory目录存放;也可以指定写到高速磁盘上,加快undo log的读写性能。

 

六.innodb_undo_log_truncate

表示是否开启自动收缩undo log的表空间的操作。如果配置为ON,并且配置了2个或2个以上的undo log表空间数据文件,当某一个日志文件的大小超过设置的最大值后,就会自动收缩表空间数据文件。

 

当回收表空间数据文件时,被回收的表空间数据文件会临时下线。为了保证undo log一直有地方可以写,此时要保证至少还有1个undo log日志文件是在线的,这就是要求innodb_undo_tablespaces >= 2的根本原因。

 

7.二进制日志(binlog)

(1)binlog的基本概念

(2)binlog日志的三种模式

(3)如何选择binlog的模式

(4)binlog写入机制

(5)redo log和binlog的区别

(6)binlog命令操作

 

(1)binlog的基本概念

binlog是一个二进制格式的文件,用于记录用户对数据库更新操作。例如更改库和表的SQL语句都会记录到binlog里,但binlog不会记录select和show这类操作。

 

一.binlog的特点

特点一:binlog在MySQL的Server层实现(存储引擎共用)

特点二:binlog为逻辑日志,记录的是一条SQL语句的原始逻辑

特点三:binlog不限制大小,追加写入,不会覆盖以前的日志

特点四:默认情况下,binlog日志是二进制格式的。不能使用查看文本工具的命令查看,而要使用mysqlbinlog解析查看

 

二.开启binlog的场景

场景一:主从复制

在主库中开启binlog功能,这样主库就可以把binlog传递给从库,从库拿到binlog后实现数据恢复达到主从数据一致性。

 

场景二:数据恢复

通过mysqlbinlog工具来恢复数据。

 

(2)binlog日志的三种模式

一.ROW模式

日志中会记录每一行数据被修改的情况,然后在Slave端对相同的数据进行修改。

 

优点是可以清楚记录每一个行数据的修改细节,能完全实现主从数据同步和数据的恢复。缺点是批量操作会产生大量日志,尤其是alter table会让日志暴涨。

 

二.STATMENT模式

日志中会记录每一条修改数据的SQL语句。注意批量修改时,记录的不是单条SQL语句,而是批量修改的SQL语句。Slave的SQL进程会解析成和原来Master执行过的相同的SQL再次执行,简称SQL语句复制。

 

优点是日志量小减少磁盘IO,提升存储和恢复速度。缺点是在某些情况下会导致主从数据不一致,如last_insert_id()、now()。

三.MIXED模式

以上两种模式的混合使用,一般会使用STATEMENT模式保存binlog,对于STATEMENT模式无法复制的操作使用ROW模式保存binlog。MySQL会根据执行的SQL语句选择写入模式。

 

(3)如何选择binlog的模式

一.如果使用MySQL的特殊功能相对少(存储过程、触发器、函数),可以选择默认的语句模式,Statement模式。

二.如果使用MySQL的特殊功能较多的,可以选择Mixed模式。

三.如果使用MySQL的特殊功能较多,又希望数据最大化一致,此时最好Row模式,但是要注意,该模式的binlog日志量增长非常快。

 

(4)binlog写入机制

一.binlog文件结构

binlog文件中记录的是对数据库的各种修改操作。binlog中用来表示修改操作的日志记录的数据结构是Log Event,不同的修改操作对应的不同的Log Event。

 

比较常用的Log Event有:Query Event、Row Event、Xid Event等,binlog文件的内容就是各种Log Event的集合。

 

二.binlog落盘策略

binlog写入顺序:Binlog Cache -> (write) OS Cache -> (fsync) Disk。其中,write表示写入操作系统缓存,fsync表示持久化到磁盘。

 

binlog刷盘由参数sync_binlog进行配置:

sync_binlog = 0,表示每次提交事务都只write,不fsync。

sync_binlog = 1,表示每次提交事务都会执行fsync。

sync_binlog = N,表示每次提交事务都write,累积N个事务后才fsync。

 

注意:不建议将sync_binlog参数设成0。比较常见的是将sync_binlog设置为100 ~ 1000中的某个数值。如果设置成0,主动重启丢失的数据不可控制。如果设置成1,效率低下。如果设置成N,则宕机最多造成N个事务的binlog丢失,可控且性能较高。

mysql> show variables like '%sync_binlog%';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| sync_binlog   | 1     |
+---------------+-------+
1 row in set (0.00 sec)

三.binlog写入流程

步骤1:根据记录模式和操作触发Event事件生成Log Event。

 

步骤2:事务执行过程中,先把日志(Log Event)写到Binlog Cache;事务提交时,再把Binlog Cache写到binlog文件中。

 

步骤3:系统会为每个线程分配一块Binlog Cache内存,每个线程都有自己的Binlog Cache,共用一份binlog文件。

 

步骤4:事务提交时,会把Binlog Cache里完整的事务写入binlog文件中,然后清空Binlog Cache。

 

(5)redo log和binlog的区别

区别一:redo log是InnoDB引擎特有的,binlog是MySQL的Server层实现的,所有引擎都可以使用。

 

区别二:redo log是物理日志,记录的是"在XXX数据页上做了XXX修改"。binlog是逻辑日志,记录的是原始逻辑,其记录是对应的SQL语句。

 

区别三:redo log是循环写,空间一定会用完,需要WritePos和CheckPoint搭配;binlog是追加写,写到一定大小会切换到下一个,不会覆盖以前的日志。

 

区别四:redo log作为服务器异常宕机后事务数据自动恢复使用,binlog可以作为主从复制和数据恢复使用,binlog没有CrashSafe(崩溃恢复)能力。

 

什么是CrashSafe?CrashSafe指MySQL服务器宕机重启后,能够保证:已提交的事务的数据仍然存在 + 没提交的事务的数据自动回滚。

 

(6)binlog命令操作

一.启用binlog

$ vim /etc/my.cnf
增加下面几个参数
binlog-format=ROW
log-bin=mysqlbinlog
server-id=1

binlog-format:文件模式。

log-bin:MySQL会根据这个配置自动设置log_bin为on状态,自动设置log_bin_index文件为指定的文件名后跟.index。

server-id=1:随机指定一个不能和其他集群中机器重名的字符串,如果只有一台机器,那就可以随便指定了。

 

二.重启mysql

$ systemctl restart mysqld

三.启动成功后登陆查看配置是否起作用

mysql> show variables like '%log_bin%';
+---------------------------------+----------------------------------+
| Variable_name                   | Value                            |
+---------------------------------+----------------------------------+
| log_bin                         | ON                               |
| log_bin_basename                | /var/lib/mysql/mysqlbinlog       |
| log_bin_index                   | /var/lib/mysql/mysqlbinlog.index |
| log_bin_trust_function_creators | OFF                              |
| log_bin_use_v1_row_events       | OFF                              |
| sql_log_bin                     | ON                               |
+---------------------------------+----------------------------------+

log_bin:是否开启binlog日志;
log_bin_basename:基础文件名;
log_bin_index:binlog文件的索引文件,管理所有binlog文件;
sql_log_bin:表示当前会话是否记录binlog,默认值ON;
当sql_log_bin关闭后,主库上的改动不记录binlog,不会复制到从库;

四.查看binlog文件列表

mysql> show binary logs;
+--------------------+-----------+
| Log_name           | File_size |
+--------------------+-----------+
| mysqlbinlog.000001 |       177 |
| mysqlbinlog.000002 |       177 |
| mysqlbinlog.000003 |       154 |
+--------------------+-----------+

五.查看正在写入的binlog文件

mysql> show master status;

六.查看binlog文件信息

使用show binlog events命令查询到的每一行数据就是一个binlog管理事件。

mysql> show binlog events;
+--------------------+-----+----------------+-----------+-------------+---------------------------------------+
| Log_name           | Pos | Event_type     | Server_id | End_log_pos | Info                                  |
+--------------------+-----+----------------+-----------+-------------+---------------------------------------+
| mysqlbinlog.000001 |   4 | Format_desc    |         1 |         123 | Server ver: 5.7.30-log, Binlog ver: 4 |
| mysqlbinlog.000001 | 123 | Previous_gtids |         1 |         154 |                                       |
| mysqlbinlog.000001 | 154 | Stop           |         1 |         177 |                                       |
+--------------------+-----+----------------+-----------+-------------+-----------

Log_name:当前事件所在的binlog文件名称;
Pos:当前事件的开始位置,每个事件都占用固定的字节大小,结束位置End_log_position减去Pos,就是这个事件占用的字节数。第一个事件位置并不是从0开始,而是从4开始。MySQL通过文件中的前4个字节,来判断这是不是一个binlog文件,很多格式文件如pdf、doc等都会用前几个特定字符判断文件是否合法;
Event_type:表示事件的类型;
Server_id:表示产生这个事件的MySQL的server_id,my.cnf中配置;
End_log_position:下一个事件的开始位置;
Info:当前事件的描述信息;

七.查看指定binlog文件内容

mysql> show binlog events in 'mysqlbinlog.000001'\G;
*************************** 1. row ***************************
   Log_name: mysqlbinlog.000001
        Pos: 4
 Event_type: Format_desc
  Server_id: 1
End_log_pos: 123
       Info: Server ver: 5.7.30-log, Binlog ver: 4
*************************** 2. row ***************************
   Log_name: mysqlbinlog.000001
        Pos: 123
 Event_type: Previous_gtids
  Server_id: 1
End_log_pos: 154
       Info: 
*************************** 3. row ***************************
   Log_name: mysqlbinlog.000001
        Pos: 154
 Event_type: Stop
  Server_id: 1
End_log_pos: 177
       Info: 
3 rows in set (0.00 sec)

八.使用mysqlbinlog命令查看binlog文件

[root@localhost mysql]# mysqlbinlog mysqlbinlog.000001
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=1*/;
/*!50003 SET @OLD_COMPLETION_TYPE=@@COMPLETION_TYPE,COMPLETION_TYPE=0*/;
DELIMITER /*!*/;
# at 4
#220125  6:40:35 server id 1  end_log_pos 123 CRC32 0x1e570724  Start: binlog v 4, server v 5.7.30-log created 220125  6:40:35 at startup
ROLLBACK/*!*/;
BINLOG '
4wvwYQ8BAAAAdwAAAHsAAAAAAAQANS43LjMwLWxvZwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAADjC/BhEzgNAAgAEgAEBAQEEgAAXwAEGggAAAAICAgCAAAACgoKKioAEjQA
ASQHVx4=
'/*!*/;
# at 123
#220125  6:40:35 server id 1  end_log_pos 154 CRC32 0x43fa19f1  Previous-GTIDs
# [empty]
# at 154
#220125  6:41:17 server id 1  end_log_pos 177 CRC32 0x205de899  Stop
SET @@SESSION.GTID_NEXT= 'AUTOMATIC' /* added by mysqlbinlog */ /*!*/;
DELIMITER ;
# End of log file
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/;

输出指定position位置的binlog日志:

$ mysqlbinlog --start-position='154' --stop-position='177'

输出指定position位置的binlog日志到指定文件中:

$ mysqlbinlog --start-position='154' --stop-position='177' mysqlbinlog.000001 > test.log

输出指定position位置的binlog日志到压缩文件中:

$ mysqlbinlog --start-position="指定开始位置" --stop-position="指定结束位置" binlog文件|gzip > 压缩文件名

输出指定开始时间的binlog日志:

$ mysqlbinlog --start-datetime="yyyy-MM-dd HH:mm:ss" binlog文件

 

8.新版本结构演变

(1)MySQL 5.7版本

一.将undo日志表空间从共享表空间ibdata文件中分离出来,但需要在安装MySQL时就要指定文件大小和数量才能分离出来。

二.增加了temporary临时表空间,存储临时表或临时查询结果集的数据。

三.Buffer Pool大小可以动态修改,无需重启数据库实例。

 

(2)MySQL 8.x版本

一.将InnoDB表的数据字典和undo从共享表空间ibdata中彻底分离出来,以前需要ibdata中数据字典与独立表空间ibd文件中数据字典一致才行,8.x版本就不需要了。

二.temporary临时表空间也可以配置多个物理文件,而且均为InnoDB存储引擎并能创建索引,这样加快了处理的速度。

三.用户可以像Oracle数据库那样设置一些表空间,每个表空间对应多个物理文件,每个表空间可以给多个表使用,但一个表只能存储在一个表空间中。

四.将Double Write Buffer从共享表空间ibdata中也分离出来了。

 

posted @ 2024-11-28 22:08  东阳马生架构  阅读(12)  评论(0编辑  收藏  举报