InnoDB数据存储及事务两阶段提交原理解析

数据库 MySQL
本文详细介绍MySQL InnoDB的数据模型、数据持久化策略、事务提交以及故障恢复原理。

1、背景和目标

1.1 背景

MySQL在互联网行业应用广泛,性能强、可靠性高,云厂商还提供了许多扩展工具,生态相对其他数据库而言比较成熟。

归因于成熟的基建,业务研发人员更需关心的是数据库设计方案、操作数据时的性能和一致性问题。如果我们在使用事务时,不知道数据存储方式和事务实现原理,往往会在一个事务的多次读写过程中产生bug,即数据的变更不符合预期。因此当了解了MySQL事务的底层实现原理,我们就能知道如何编写代码以达到预期,就能知道数据库引擎设计的精妙之处。

1.2 目标

详细介绍MySQL InnoDB的数据模型、数据持久化策略、事务提交以及故障恢复原理。

2、InnoDB存储结构

2.1 InnoDB逻辑存储结构

InnoDB逻辑存储结构层级:表空间->段->区->页->行

图片

如上图所示,数据表有许多数据行,分别存储在16KB的Page上,把一定数量的Page整合为了一个Extent(默认是64个Page即共1M),而多个Extent又构成了一个Segment,不同类型的Segment又组成了对应类型的表空间。

2.2 InnoDB物理存储结构

InnoDB总体结构分为内存结构(下图左侧)和磁盘结构(右侧)两部分。

图片

3、InnoDB磁盘结构详解

3.1 表空间

磁盘部分包括各种表空间,包括系统表空间(System Tablespace)、独立表空间(File-Per-Table Tablespaces)、undo表空间(Undo Tablespaces)、通用表空间(General Tablespaces)、临时表空间(Temporary TableSpaces)5种表空间。

表空间可以看做是InnoDB存储引擎逻辑结构的最高层 ,所有的数据都是存放在表空间中。InnoDB通过参数InnoDB_file_per_table(DMS是ON)可以选择使用系统表空间还是独立表空间存储表,如果不是ON,则所有InnoDB表都保存在ibdata1这个表文件中,否则一个表占据一个表文件,拥有自己独立的表文件(用户记录、索引和插入缓冲Bitmap),即每个Table单独存储为一个“.ibd”文件,但change buffer等依然存放在系统表空间。

3.2 段

多个段组成一个表空间。常见的段有数据段、索引段、回滚段等,段是一个逻辑的概念,是一些零散页面和一些完整的区的集合。不同类型的数据保存在单独的段内,可以更好的保持该类型数据的连续性,可以提升访问磁盘的效率。创建一个索引会创建数据段和索引段,即一个索引占用两个段。

  • 数据段:B+树的叶子节点(Leaf node segment)
  • 索引段:B+树的非叶子节点(Non-leaf node segment)
  • 回滚段(rollback segment):InnoDB中undo log是采用分段(segment)的方式进行存储的,每一个rollback segment内部由1024个undo segment组成,每个undo Tablespace最多会包含128个rollback segment。每一时刻一个undo segment都是被一个事务独占的,每个写事务都会持有至少一个undo segment,当有大量写事务并发运行时,就需要存在多个undo segment。MySQL 8.0由于支持了最多128个独立的Undo Tablespace,一方面避免了ibdata1的膨胀,方便undo空间回收,另一方面也大大增加了最大的rollback segment的个数,增加了可支持的最大并发写事务数(128*128*1024)。

注意,虽然InnoDB区分了数据段和索引段,但由于数据是以主键为索引来组织数据的存储的,所以索引文件和数据文件都在同一个文件中,都在“.ibd”文件里面。

3.3 区

表空间中的页实在是太多了,为了更好的管理这些页面,InnoDB提出了区的概念。一个表空间划分为多个区(extent),一个区内包含物理上连续的64个页,因此一个区空间大小为64*16KB=1M。区就是为了保证页的连续性,InnoDB一次会从磁盘申请4~5个区。

段可以简单理解为是一个逻辑的概念,而Extent是一个物理概念,每次B+树的扩容都是以Extent为单位来扩容的,默认一次扩容不超过4个Extent。

段区分了数据段和索引段,其实也就有了各自的区,即叶子节点和非叶子节点都有自己独立的区。想象一下,当B+树按顺序范围查询时,如果数据分布在磁盘的不同位置,就会产生随机IO,而如果数据的物理位置相邻,就可以通过顺序IO读取了。

3.4 页

页是InnoDB中管理数据的最小单元,是固定大小的一段连续磁盘空间,默认为16KB,用于存放数据、索引等各种类型的数据。

InnoDB中,常见的页类型有数据索引页、undo page、文件管理页FSP_HDR/XDES、插入缓冲IBUF_BITMAP页、INODE页等。

在InnoDB中的设计中,页与页之间是通过一个双向链表连接起来,而存储在页中的数据行则是通过单链表连接起来的,如下图:

图片

页有通用的文件头和尾(将页的内容进行封装,通过文件头和文件尾的checksum方式来确保页的完整性),但是中部的内容根据页的类型不同而发生变化。我们主要关注数据页和索引页,这种类型的页包括七个部分:

  • File Header:文件头,共38B,记录了页的地址、页号、上一页和下一页指针、页的类型信息、页的校验和checksum(校验和在写入磁盘前计算得到,当从磁盘中读取时,重新计算校验和并与数据页中存储的对比,如果发现不同,则会导致MySQL crash)、日志序列位置(LSN,Log Sequence Number,表示日志文件的长度,一个不断递增的unsigned long类型整数)等。
  • Page Header:数据页头,用来记录数据页的状态信息,包括Free Space的地址、本页中的记录的数量、标记为删除的记录等,共56B。
  • System records:Infimum + Supremum Records。InnoDB每页中有两个虚拟的行记录,用来限定记录的边界。Infimum记录是比该页中任何主键值都要小的记录,Supremum记录是比该页中任何主键值都要大的记录。这两个记录在页创建时被建立,并且在任何情况下不会被删除,并且由于这两条记录不是我们自己定义的记录,所以它们并不存放在页的User Records部分。所以如果数据是顺序存储的,那么查询数据是否在某一页中就无需遍历页中的所有数据,只需判断这两个记录就行了。
  • User Records:用户记录,以单链表的形式存储,如下图:

图片

  • Free Space:空闲空间,用于存放新记录。在一开始生成页的时候,并没有User Records这个部分,每当插入一条记录,就会从Free Space部分中申请一个记录大小的空间到User Records部分,当Free Space用完时,这个页也就使用完了。
  • Page Directory:数据目录(弥补单向链表查询性能差的缺点),InnoDB会把页中的记录划分为若干个组,每个组的最后一个记录的地址偏移量作为一个槽,存放在Page Directory中,便于二分查找定位数据。对于分组中的记录数是有规定的:Infimum记录所在的分组只能有 1 条记录,Supremum记录所在的分组中的记录条数只能在1~8条之间,中间的其它分组中记录数只能在是4~8条之间。所以如果数据是顺序存储的,那么查询数据在某一页的位置就无需遍历页中的所有数据,只通过二分法就可以快速定位到对应的槽,然后再遍历该槽对应分组中的记录就能知道了。
  • File Trailer:文件尾,共8B,包括页的校验和checksum(依赖于引擎选用的校验算法,不一定与文件头的checksum相同)、日志序列位置(LSN),与File Header中的相同。默认情况下,InnoDB每次从磁盘读取一个页就会检测该页的完整性,即File Trailer中的内容需和File Header保持一致。

3.5 行

数据行即一行一行的数据。MySQL中单行数据最大能存储64KB=65535B,故表中字段长度加起来如果超过该值就会拒绝创建表。以utf8mb4字符集下varchar(M)为例,该字符集下一个字符最多需要4B表示,如果M大于16383,那么总字节数就会超过4*16383=65532B,所以M的最大值就是16383个字符。

虽然单行数据最大值远大于单页(16KB),但MySQL为了在单页中至少存储2行数据(每行8KB),引入了行溢出机制,即只要一行记录的总和超过8KB,就会溢出,比如varchar(9000) 或者 varchar(3000) + varchar(3000) + varchar(3000),当实际长度大于8k的时候,会对最大字段使用uncompress BLOB page单独存储(即一个字段独享一个或多个页),而在Barracuda文件格式下字段本身只会用20B存储溢出行的地址和占用的字节数。

InnoDB的文件格式包括旧格式Antelope和新格式Barracuda(DMS使用该格式),两者主要的不同在于对存储数据时所占用的空间差异,每种文件格式有自己支持的行格式,行格式就是指数据行的存储方式,包括是否紧凑存储(占用磁盘空间)、是否可变长度存储、大索引前缀支持、压缩支持。差异如下:

行格式

紧凑的存储特性

增强的可变长度列存储

大索引键前缀支持

压缩支持

支持的表空间类型

所需文件格式

REDUNDANT(冗余)

system, file-per-table, general

Antelope or Barracuda

COMPACT(紧凑)

system, file-per-table, general

Antelope or Barracuda

DYNAMIC(动态)

system, file-per-table, general

Barracuda

COMPRESSED(压缩)

file-per-table, general

Barracuda

通过下列指令可以查询到数据库的文件格式和行格式配置:

show variables like "InnoDB_file_format";
show variables like "InnoDB_default_row_format";

REDUNDANT和其他几种类型的区别在就是在于首部的内容区别。REDUNDANT的存储格式为首部是一个字段长度偏移列表(每个字段占用的字节长度及其相应的位移),其他类型的存储格式为首部是一个非NULL的变长字段长度列表,这种方式存储数据会更加紧凑(页中存放的行数越多,性能就越高),数据布局如下图:

  • 针对VARCHAR、TEXT、BLOB这类变长字段,列中实际存储了多少数据是不固定的,因此除了要把数据本身存下来,还需要记下它的长度。
  • 如果字段值为NULL,其并不占该部分任何空间,除了占有NULL标志位,故两个字段为NULL就占用2bit。
  • 头信息中包括删除标记、当前记录是否是分组中的最后一条、当前记录在页中的相对位置、记录类型(0:普通记录,1:B+树非叶子节点目录项记录,2:Infimum记录,3:Supremum记录)、下一条记录的相对位置等。
  • 每行数据除了用户定义的列外,还有3个隐藏列,包括trx_id列和roll_pointer列(见下文),分别为6字节和7字节的大小,若表没有定义主键,每行还会增加一个6字节的rowid列。

注意,索引也是按这种方式存储的:

  • 对于聚簇索引,非叶子节点包含主键和child page number,叶子节点包含主键和具体的行;
  • 对于非聚簇索引,也就是二级索引,非叶子节点包含二级索引和child page number,叶子节点包含二级索引和主键值。

4、InnoDB内存结构详解

4.1 buffer pool

buffer pool是InnoDB的缓存,用来存放各种数据,包括索引页(index page)、数据页(data page)、undo页、插入缓冲、自适应哈希索引(AHI)、innodb存储的锁信息、数据字典等。把磁盘上的数据加载到缓冲池中(通过预读机制加载当前页、相邻页),可避免每次访问都进行磁盘IO,起到加速访问的作用。应用程序在对数据库执行增删改操作的时候,实际上主要都是针对内存里的buffer pool中的数据进行的。

buffer pool包含三种数据类型:

  • free page:从未用过的页。
  • clean page:干净的页,即数据页的数据和磁盘一致。
  • dirty page:脏页,即数据页的数据和磁盘不一致。

针对这3种页,InnoDB使用3种链表维护:

  • free list:空闲页链表,管理free page。
  • flush list:脏页链表,管理dirty page并在某个时刻对该链表的脏页进行刷盘,按脏页的修改时间排序,更新操作较早的脏页先被刷盘。
  • lru list:正在使用的内存页链表,里面包含clean page和dirty page,也就是说lru list中的页包含flush list中的所有脏页。lru list遵循lru算法管理缓存页。

InnoDB需要保证buffer pool的数据都是热点数据,将无效的预读数据快速删除、不将读入后立即使用的数据替换热点数据,就引入了变种lru算法(新生代+老生代、老生代停留时间窗口)来解决“预读失效”与“缓冲池污染”的问题。通过下列指令可以查询到数据库设置冷热分界线和成为热块的所需时间:

show variables like 'InnoDB_old_blocks_pct'; -- 单位%,默认37,代表冷数据占比
show variables like 'InnoDB_old_blocks_time'; -- 单位ms,默认1000

Mysql5.7.5之后,buffer pool有分块(chunk)的特性,即一个buffer pool实例是由多个块组成,每个块的块内空间是连续的,块与块之间则是离散的。分块是为了方便用户在mysql运行期间能够调整buffer pool的大小。

注意,为了提高读写性能,避免过少的数据刷盘或随机IO,buffer pool一般不会对单个Page实时刷盘,所以这就出现了缓存和磁盘的一致性问题,InnoDB通过引入redolog来保存数量操作记录从而解决此问题,见下文。

4.2 change buffer

change buffer(写缓存)是一种特殊的数据结构,可以避免数据更改时因为隐式查询数据带来的磁盘IO。change buffer默认占buffer pool的 25%,最大允许占50%。可以根据写业务的量调整,写操作越频繁,change buffer带来的性能提升越明显。

change buffer工作原理如下:

  1. 当更改的页存在于buffer pool的lru list,则直接在缓冲池中修改这个页,这个页会变成脏页,链入到flush list中,但并不马上刷盘;此时不涉及change buffer操作。
  2. 当更改的页不存在于buffer pool的lru list,就要先从磁盘读取要修改的数据页到buffer pool后再修改(数据不会在磁盘中直接更改)。但为了避免修改操作引发的磁盘读IO,系统会将DML操作记录到change buffer中,并不马上刷盘。等下次对这些修改的页进行查询时,由于lru list不存在该页,会从磁盘读取(磁盘页是更改前的数据),为了避免读到脏数据,该磁盘页会和change buffer中的更改合并后才链入到lru list。如果未来一段时间都不会查询到这个修改了的页,也会有insert buffer thread定时将change buffer的数据合并到磁盘页中。
  3. 如果做出的更改是对唯一键索引的值的修改,InnoDB要做唯一性校验,必须查询磁盘,再在lru list上的页修改,不会在change buffer中操作。

综上:change buffer适合写多读少的场景,并且满足非唯一索引。

4.3 Adaptive Hash Index

Adaptive Hash Index(AHI,自适应哈希索引),是指InnoDB存储引擎通过监控表上索引页的查找模式,自动根据查找模式对“热点数据”来创建哈希索引。因为对B+树索引的访问需要依次访问根节点>中间节点>叶子节点,而对哈希索引的访问仅需要一次HASH计算即可定位到目标位置。一些资料统计,启用AHI后,读取和写入速度可以提高2倍,辅助索引的连接操作性能可以提高5倍。

通过下列指令可以查询到数据库的相关设置:

show variables like '%hash_index';(DMS设置的是OFF)

AHI使用条件:

  1. 索引被访问了17次(BTR_SEARCH_HASH_ANALYSIS)
  2. 索引中的某个页已经被访问了至少100次(BTR_SEARCH_BUILD_LIMIT)
  3. 数据页被相同模式(相同的查询条件)访问N次(N=页中记录*1/16)

AHI使用buffer pool中的数据页进行构造,仅保存在内存中,且仅对热点数据进行处理,因此构造AHI速度极快。

4.4 log buffer

log buffer就是redolog buffer的简称,是存储要写入磁盘上的redolog的内存区域。

log buffer由变量innodb_log_buffer_size定义大小,默认为16MB(DMS中设置了8GB)。log buffer的内容会根据设置刷盘,足够大的log buffer可以使得大事务完全依赖缓存运行,而不需要在事务提交前将redolog数据写入磁盘。因此,如果有更新、插入或删除许多行的事务,增加log buffer的大小可以节省磁盘I/O。

log buffer是顺序写的,刷盘也是顺序的,所以当某个脏页对应的redolog从log buffer刷盘时,会保证将在其之前产生的redolog也刷盘,详情见下文redolog的介绍。

5、三种log类型和作用

5.1 undolog

undolog是InnoDB的日志,又称撤销日志文件,属于逻辑日志。undolog内存数据存储在buffer pool中,磁盘数据则存储在undo tablespace。

undolog保存类型为FIL_PAGE_UNDO_LOG在undo page中,一个undo page可以保存多条undolog记录。每条undolog记录包含该undolog在undo page的页内地址、undolog对应的记录所在的tableId(tableId全局唯一)、undolog类型、undolog编号、下一条undolog的地址、old_trx_id、old_roll_pointer、主键的每个列占用的存储空间大小和真实值、被修改字段的修改前后信息等。

undolog提供回滚和多个行版本控制(MVCC)的两个能力,保证了事务的原子性:

  • 回滚:undolog分为3类,包括TRX_UNDO_INSERT_REC、TRX_UNDO_DEL_MARK_REC、TRX_UNDO_UPD_EXIST_REC,分别对应增、删、改操作。上文讲到每一行数据有两个隐藏字段trx_id和roll_pointer,它们主要作用于数据库的事务。所谓的事务就是指对一个或多个数据库的一系列操作,这些操作需保证ACID的规则。当某个事务执行过程中对某个表执行了增、删、改操作,InnoDB会给该事务分配一个递增的独一无二的trx_id(全局变量,每增加256时会刷盘),而且会生成对应的undolog,而roll_pointer就是一个指向记录对应的undolog的一个指针,数据行会存储最近提交的trx_id和roll_pointer。事务开启后,在本事务或其他事务(根据事务隔离级别)对该数据行的一次次修改中,生成的undolog会记录old_trx_id(修改该记录的上一次的trx_id)和old_roll_pointer(对应undolog地址),这样就生成了一个版本链,当需要回滚时就能沿着old_trx_id和old_roll_pointer找到一条记录的所有历史版本,从而实现事务的回滚能力。
  • MVCC:InnoDB复用了undolog中已经记录的历史版本数据来实现MVCC机制。当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过undolog读取之前的行版本信息,以此实现非锁定读取。此外,根据trx_id是递增的特性,InnoDB还引入了ReadView机制,用于保存创建事务时的活跃trx_id。ReadView有三个属性,分别是m_ids(活跃的trx_id列表)、min_trx_id(活跃的最小trx_id)、max_trx_id(下一个该分配的trx_id),基于这三个属性,实现了READ COMMITTED和REPEATABLE READ两种隔离级别。

因为一个事务可能包含多个增、删、改操作,为了提高并发执行多个事务写入undolog的性能,InnoDB将各个事务的各种操作通过上文提到的undo segment分开存储(undo segment的undo page通过链式存储,即每个事务都有自己的insert undo链表、update undo链表),而每个段的第一个undo page通过TRX_UNDO_STATE属性存储了该段的一些事务信息,取值有下面几个:

  • TRX_UNDO_ACTIVE: 活跃状态,即一个活跃的事务正在往这个段里边写入undolog。
  • TRX_UNDO_CACHED:被缓存的状态,即该状态下的段等待着之后被其他事务重用。
  • TRX_UNDO_TO_FREE: 可以释放,对于insert undo链表来说,如果在它对应的事务提交之后,该链表不能被重用,那么就会处于这种状态。
  • TRX_UNDO_TO_PURGE: 可以清理,对于update undo链表来说,如果在它对应的事务提交之后,该链表不能被重用,那么就会处于这种状态。
  • TRX_UNDO_PREPARED: 准备状态,还未提交。

在事务未提交前TRX_UNDO_STATE是TRX_UNDO_PREPARED状态,事务提交后,根据不同的操作类型转换成TRX_UNDO_CACHED、TRX_UNDO_TO_FREE或者TRX_UNDO_TO_PURGE状态,表示满足一定条件后可以清理这些undolog,事务如果需要回滚的话,必须是TRX_UNDO_ACTIVE或者TRX_UNDO_PREPARED状态,故事务的提交是由该属性判断的,详情见下文的事务执行流程。

5.2 redolog

redolog是InnoDB存储引擎层的日志,又称重做日志文件,属于物理日志。redolog内存数据存储在log buffer中,磁盘数据则存储在以ib_logfile0、ib_logfile1…命名的日志文件中。

上文提到,InnoDB通过buffer pool(包括change buffer、undolog)提高读写性能,但如果进程或机器崩溃会导致缓存丢失,为了能实现故障恢复就引入了redolog。事务在执行过程中对数据库所做的所有修改(聚集索引、二级索引、undolog等修改)都会生成对应的redolog,并保证redolog早于缓存落盘(WAL机制),当故障发生后,InnoDB会在重启时,通过重放redolog来恢复所做的修改。

到MySQL8.0为止,为了应对各种各样不同的需求,InnoDB已经有多达65种(上限127种)的redolog类型用来记录各种信息,而恢复数据时需要判断不同的类型,来做对应的解析。redolog长度是动态的,常见的数据结构包括日志类型、Space ID、页号、数据页中的偏移量、修改的长度和具体的值。

图片

根据redolog不同的作用对象,可以将这些类型划分为三个大类:作用于Page、作用于Space以及提供额外信息的Logic类型。redolog记录的是作用于页的,如果作用于Space,那么页号的值为0。

不管是在内存还是磁盘中,redolog都以块为单位进行存储,默认每个块占512B,等于磁盘扇区的大小,这称为redolog block。每个redolog block由3部分组成:日志块头(12B)、日志块尾(4B)和日志主体(492B),log buffer则是由若干个连续的redolog block组成的,总数不能超过1GB个(基于LSN的长度限制)。

InnoDB为了提高redolog的性能和保证数据一致性,还引入的mini-transaction机制(简称mtr),mtr就是redolog组的概念,比如对一些页面的访问、向聚簇索引或二级索引插入一条记录等操作时产生的redolog是不可分割的(插入数据如果引起索引分裂,会产生许多redolog)。每组的最后一条redolog后边会加上一条类型为MLOG_MULTI_REC_END的redolog,来标识该组的结束。

log buffer中写入redolog的过程是顺序的,但不是一条一条写入,而是一个mtr完成后,将里面所有的redolog一起复制到log buffer中(还会把执行过程中可能修改过的页面加入到Buffer Pool的flush链表),也就是存储到redolog block中,可能占用不到一个block,也可能占用多个block。一个事务可以包含多个mtr,那么多个事务的mtr就会有交集,事务间的mtr会相互穿插。

5.3 binlog

binlog是属于MySQL Server层面的,又称为归档日志,属于逻辑日志,是以二进制的形式记录的,是sql语句的原始逻辑,主要是用于进行集群中保证主从一致以及执行异常操作后恢复数据。

binlog日志文件默认大小由磁盘决定,顺序追加写入。binlog内存数据存储在binlog cache中(大小由binlog_cache_size控制),磁盘数据则存储在binlog file中。

binlog有三种格式,分别是Row、Statement、Mixed。

  • Row格式记录了操作语句对具体行的操作以及操作前的整行信息,缺点是占空间大(一条sql影响的行数),优点是能保证数据安全,不会发生遗漏,是5.7版本默认格式。
  • Statement格式记录了修改的sql(只是一条sql语句),缺点是在集群中可能会导致操作不一致从而使得数据不一致,如执行now()函数可能会导致不同机器值不同。
  • Mixed格式会针对于操作的sql选择使用Row还是Statement,相比于Row更省空间,但还是可能发生主从不一致的情况。

binlog和redolog虽然都保存了记录的修改日志,但两者有一些区别:

  • binlog是逻辑日志,记录的是对哪一个表的哪一行做了什么修改;redolog是物理日志,记录的是对哪个数据页中的哪个记录做了什么修改。
  • binlog是追加写;redolog是循环写,日志文件有固定大小,会覆盖之前的数据。
  • binlog是Server层的日志;redolog是InnoDB的日志。如果不使用InnoDB引擎,就没有redolog。

6、InnoDB持久化策略

6.1 InnoDB两种持久化策略

InnoDB内存部分包括缓冲池(buffer pool) 和日志缓冲(log buffer),两者刷盘方式不同,前者走direct_io模式(直接绕过Page Cache来访问磁盘),后者走Page Cache模式(IO操作需要委托操作系统来完成)。

  1. 是否使用Page Cache的区别是什么?

OS的Page Cache对读写做了不少优化,包括按顺序预读取(按页读取)、在成簇磁盘块(n次方个扇区)上执行IO、允许访问同一文件的多个进程共享高速缓存的缓冲区等,但数据必须在用户进程与内核互相拷贝。

direct_io的优点是减少操作系统缓冲区和用户地址空间的拷贝次数,降低了CPU和内存带宽的开销。而InnoDB本身也已处理好buffer pool与磁盘数据的对应关系,所以可以舍去Page Cache。

  1. 先刷buffer pool还是先刷log buffer?

先写日志,再写磁盘(WAL机制,Write-Ahead Logging),即redolog和binlog等日志数据刷盘到log文件完成后,才会将脏页从buffer pool刷盘到表文件。

为什么运用WAL机制?因为顺序写磁盘的性能堪比写内存,所以写日志会比数据刷盘的性能高很多,只要保证日志写入成功,再通过代码保证日志和需刷盘数据的一致性,就能在保证数据不丢失的情况下大大提高性能。

顺序写运用很广泛,比如kafka追加写实现了事务消息,即提交或回滚事务时,会追加写入一条控制类型的消息来标识是commit或rollback。

6.2 buffer pool持久化过程

buffer pool刷盘时机主要有以下四种:

  • MySQL正常关闭之前,会把所有的脏页刷盘;
  • Master Thread会以每秒或者每10秒一次的频率定期将适量的脏页刷盘。上文讲到buffer pool通过变种lru算法区分冷热数据,故后台线程会优先刷冷数据,因为热数据在短时间可能被多次修改,如果优先刷盘热数据页,这个页很快又会被修改,又需要再刷盘,不如等它变成冷数据再刷盘。
  • lru空闲列表不足、log buffer或磁盘空间不足时,page cleaner线程会异步将脏页刷盘。
  • buffer pool空间不足时,用户线程从磁盘读取某个页要链入lru list,lru list会释放尾部的一个页。假设这个释放的页是一个脏页,那么用户线程就不得不亲自把这个脏页刷盘,这样就会降低响应用户请求的速度。之所以需要后台线程定时刷盘脏页就是为了尽可能避免发生这种主动刷盘的情况。

InnoDB还引入了double write buffer物理存储空间,来处理buffer pool刷盘时的异常情况。buffer pool的脏页要刷盘时,数据页的空间为16KB,OS文件系统的页空间一般为4KB,磁盘的扇区每片一般为512B,最终都会一片片的刷扇区。计算机硬件和操作系统,在极端情况下(比如断电)往往并不能保证这一操作的原子性,如果16KB的数据在写入4KB时发生了系统断电/os crash,只有一部分写是成功的,这种情况写就是partial page write

mysql在恢复的过程中是检查页的checksum(页的校验和,见上文),发生partial page write问题时, Page已经损坏,找不到该页的checksum,就无法通过redolog恢复。

因此根据上述问题,InnoDB将buffer pool中的脏页刷盘时,会先通过memcpy函数将Page刷到double write buffer,再将数据拷贝到数据文件对应的位置。

double write buffer是物理磁盘上共享表空间中连续的128个页(每页16KB,大小共2MB, 每次写入1MB)。

  • 如果写double write buffer失败,那么这些数据不会刷盘,InnoDB会载入磁盘原始数据和redo日志比较,并重新刷到double write buffer,然后再刷盘。
  • 如果写double write buffer成功,但是刷盘失败(partial page write问题),那么InnoDB就不会通过事务日志来恢复了,而是直接用double write buffer中的数据刷盘。

6.3 redolog持久化过程

redolog包括两部分:一是内存中的日志缓冲(log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redolog file,以ib_logfile0、ib_logfile1…命名),该部分日志是持久的。

redolog可以通过参数InnoDB_log_files_in_group配置成多个文件(最大100),另外一个参数InnoDB_log_file_size表示每个文件的大小,因此总的redolog大小为InnoDB_log_files_in_group * InnoDB_log_file_size。

上文讲到内存中log buffer是由多个redolog block组成的(日志块头占12B、日志块尾占4B),那么redolog file也是如此,每个redolog file的前4个block用于表示文件头,存储了一些管理信息,往后则存储log buffer中的block镜像。文件头主要存储了标记redolog file开始的LSN值(Log Sequence Number的简称)、标记redolog已刷盘的全局变量flushed_to_disk_lsn值、标记脏页已刷盘的全局变量checkpoint_lsn等。

  • LSN:LSN记录了已经写入的redolog的日志量,是一个全局变量,初始值为8704。每次写入一个mtr时,LSN就会累加上mtr所占的空间字节数和相应的block头尾空间字节数。比如mtr_1产生的redolog为200B,那么LSN就变成了8704+12+200=8916,之后mtr_2又产生了1000B的redolog,那么LSN就变成了8916+296+4+512+12+208=9948。
  • flushed_to_disk_lsn:系统第一次启动时,flushed_to_disk_lsn值和初始的LSN值是相同的,都是8704。随着系统的运行,redolog被不断写入log buffer,但是并不会立即刷盘,LSN的值就和flushed_to_disk_lsn的值拉开了差距,如果两者的值相同时,说明log buffer中的所有redolog都已经刷盘了。
  • checkpoint_lsn:checkpoint_lsn的初始值也是8704,当flush链表中的脏页按顺序被刷盘时,mtr生成的对应redolog就可以被覆盖了,所以我们可以进行一个增加checkpoint_lsn的操作,我们把这个过程称之为做一次checkpoint。脏页是与redolog有关联的,记录了redolog的LSN信息,通过脏页可以找到对应的redolog,通过redolog也可以恢复对应的脏页。

下图展示了一组4个文件的redolog日志,checkpoint_lsn之前的空间表示可以进行写的文件。

图片

我们再看下log buffer刷盘的具体过程:

  1. 客户端向数据库发送写命令。
  2. 数据库收到写命令。
  3. 数据库通过系统调用将数据写入内核缓冲区(Page Cache)。
  4. 操作系统将缓冲区数据传输至磁盘控制器,暂存在磁盘缓冲区。
  5. 磁盘控制器将数据精准的写入物理磁盘。

如果数据库停机,那么第三步之后操作系统可以保证数据写入磁盘;如果是操作系统停机,此时磁盘也无法正常工作,那就必须完成这五步才能保证数据落盘。

如上所述,在将写操作写入redolog的过程中也不是直接就进行磁盘IO来完成的,而是分为三个步骤:

图片

  1. 写入log buffer中,这部分是属于MySQL的内存中,是全局公用的。
  2. 在事务编写完成后,就可以执行write操作,写到文件系统的Page Cache中。
  3. 执行fsync(持久化)操作,将Page Cache中的数据正式写入磁盘上的redolog文件中,也就是图中的hard disk。

InnoDB_flush_log_at_trx_commit参数控制了log buffer的刷盘时机(值可为0、1、2,默认1):

  • 设置为0:每隔1秒从log buffer写入Page cache,并马上刷盘,mysql服务故障或者主机宕机则丢失1秒(由log buffer的innodb_flush_log_at_timeout参数控制)数据。
  • 设置为1:事务提交时,立刻从log buffer写入Page cache, 并马上刷盘,mysql服务故障或者主机宕机不会丢失数据,但会频繁发生磁盘IO。
  • 设置为2:事务提交时,立刻从log buffer写入Page cache,每隔1秒刷盘,mysql服务故障不会丢失数据,因为数据已经进入操作系统缓存,与mysql进程无关了,主机宕机则丢失1秒数据。

除此之外,当log buffer空间不足、做checkpoint、Mysql正常关闭、binlog切换等情况也会触发redolog刷盘。刷盘操作是异步IO,由专门的线程完成这件事,不会阻塞用户请求的处理。redolog如果没有及时刷盘或者只刷盘一部分,是会导致事务丢失的。

6.4 undolog持久化过程

InnoDB的undolog严格的讲不是Log,而是数据,因此他的管理和落盘都跟数据一样:

  • undolog的磁盘结构并不是顺序的,而是像数据一样按Page管理。
  • undolog写入时,也像数据一样产生对应的redolog。
  • undolog的Page也像数据一样缓存在Buffer Pool中,跟数据Page一起做lru换入换出,以及刷脏。undo page的刷脏也像数据一样要等到对应的redolog落盘之后。

之所以这样实现,首要的原因是undolog需要承担MVCC对历史版本的管理作用,设计目标是高事务并发,方便的管理和维护,因此当做数据更合适。

6.5 binlog持久化过程

binlog也有独立的刷盘策略,通过sync_binlog参数控制(值分别为0、1、N,默认为1):

  • 设置为0 :每次提交事务都只将binlog cache进行write,不fsync。
  • 设置为1 :每次提交事务都会将binlog cache进行write,并执行fsync。
  • 设置为N :表示每次提交事务都会将binlog cache进行write,但累积N个事务后才fsync。

由于binlog是属于MySQL Server层面的日志,只需追加写入即可。

7、MySQL事务提交和崩溃恢复

7.1 MySQL中的XA协议

有一个名叫X/Open的组织提出了一个名为XA的规范。这个XA规范提出了2个角色:

  • 一个全局事务由多个小的事务组成,所以我们得在某个地方找一个总揽全局的角色用于和各个小事务进行沟通,指导它们是提交还是回滚。这个角色被称作事务协调器(Transaction Coordinator)。
  • 管理一个小事务的角色被称作事务管理器(Transaction Manager)。

要提交一个全局事务,那么属于该全局事务的若干个小事务就应该全部提交,只要有任何一个小事务无法提交,那么整个全局事务就应该全部回滚。XA规范中指出,要提交一个全局事务,必须分为2步:

  • Prepare阶段:当协调器准备提交一个全局事务时,会依次通知各个管理器把在事务执行过程中所产生的数据都刷盘。
  • Commit阶段:如果在Prepare阶段各个管理器都完成了数据的刷盘,那么协调器就要真正通知各个管理器去提交事务了,否则就需要让这些管理器回滚事务了。

XA规范把上述全局事务提交时所经历的两个阶段称作两阶段提交。在单个MySQL实例中,将server层作为事务协调器,存储引擎作为事务管理器,故本文将binlog作为事务协调器。

7.2 sql执行流程

sql提交到MySQL时需要进行词法语法分析、优化(如果没有命中索引,就会扫全表),才会执行:

图片

7.3 事务执行流程

假设我们要更新一条数据,语句如下:

update T set c=c+1 where ID=2;

  1. Server层的执行器先调用引擎取出ID=2这一行。ID是主键,引擎直接用树搜索找到这一行。如果 ID=2这一行所在的数据页本来就在内存中,就直接返回给执行器;否则需要先从磁盘读入内存,然后再返回。
  2. 执行器拿到数据把这个值+1(分配trx_id,开始记录事务),得到新的数据,再调用存储引擎接口写入这行新数据。此处会先记录undolog,并将undolog对应的变化信息redolog保存到log buffer中,然后再去修改buffer pool,并且把buffer pool对应的变化信息redolog记录到log buffer中,详情见上文。
  3. InnoDB做完上述操作后,就准备提交事务了。此时处在Prepare阶段,执行器调用binlog_prepare接口,就会将上文提到的undo segment的状态置为TRX_UNDO_PREPARED,并将本次提交事务的XID也写入其中,同时生成对应的redolog。此时根据redolog的刷盘策略,本次事务对应的log buffer可能会被刷盘,而只要log buffer刷盘成功,那么即使之后系统崩溃,在重启恢复的时候也可以将处于Prepare状态的事务完全恢复(恢复buffer pool和undolog),然后回滚或者再次提交事务。
  4. 而到Commit阶段,执行器继续调用binlog_commit接口提交事务,此时会先将事务执行过程中产生的binlog(包括XID)按照binlog的刷盘策略刷入磁盘,再根据不同的操作类型把undo segment的状态转换成TRX_UNDO_CACHED、TRX_UNDO_TO_FREE或者TRX_UNDO_TO_PURGE(这几个状态是InnoDB的事务结束的标志),表示满足一定条件后可以清理这些undolog,并将对应的redolog刷盘。至此这个事务就算是提交完了,注意事务提交需要三次刷盘(写redolog,写binlog,写commit,InnoDB新版本通过组提交进行了优化)。而脏页并不一定随着事务提交而刷盘,需依赖于buffer pool持久化策略。
  5. 对于处于Prepare状态的事务,存储引擎既可以提交,也可以回滚,这取决于目前该事务对应的binlog是否已经写入硬盘。这时就会读取最后一个binlog日志文件,从日志文件中找一下有没有该Prepare事务对应的XID记录,如果有的话,就将该事务提交,否则就回滚好了。

7.4 如果没有两阶段提交

redolog未写入,binlog未写入:此时MySQL异常重启无法恢复数据,认为sql就没执行。

redolog写入,binlog未写入:此时MySQL异常重启能根据redolog恢复事务提交时的数据,但binlog没有记录,后续使用binlog恢复临时库会出现数据丟失,导致状态不一致。

binlog写入,redolog未写入:此时MySQL异常重启临时库能根据binlog重放事务提交时的数据,但redolog没有记录,如果主库有一些脏页已经刷盘,本应先回滚再通过binlog重放,但现在无法回滚,会导致状态不一致。

7.5 结论

所谓两阶段提交,就是指同时将redolog和binlog都写成功,这样既能保证通过binlog恢复临时库时和主库无差异,又能保证通过redolog恢复主库时和临时库无差异。

责任编辑:庞桂玉 来源: 得物技术
相关推荐

2023-07-26 09:24:03

分布式事务分布式系统

2022-03-28 10:44:51

MySQL日志存储

2024-01-26 08:18:03

2018-10-29 08:44:29

分布式两阶段提交事务

2023-11-29 07:47:58

DDIA两阶段提交

2023-12-05 09:33:08

分布式事务

2017-08-30 18:15:54

MySql

2021-01-12 14:46:34

Kubernetes开发存储

2020-02-03 12:12:28

MySQL数据库SQL

2023-01-18 10:35:49

MySQL数据库

2021-07-23 13:34:50

MySQL存储InnoDB

2024-03-26 16:24:46

分布式事务2PC3PC

2021-10-12 19:12:15

单步实现系统

2023-11-16 09:01:37

Hadoop数据库

2022-07-27 08:52:10

MySQL二阶段提交

2023-10-24 08:25:20

TCC模式事务

2015-10-29 13:52:53

MySQLinnodb引擎备份

2022-08-28 09:05:34

分布式存储Ceph

2023-12-26 08:08:02

Spring事务MySQL

2010-07-06 10:07:10

jQueryJSON
点赞
收藏

51CTO技术栈公众号