MySQL在互联网行业应用广泛,性能强、可靠性高,云厂商还提供了许多扩展工具,生态相对其他数据库而言比较成熟。
目前创新互联已为成百上千家的企业提供了网站建设、域名、雅安服务器托管、绵阳服务器托管、企业网站设计、江岸网站维护等服务,公司将坚持客户导向、应用为本的策略,正道将秉承"和谐、参与、激情"的文化,与客户和合作伙伴齐心协力一起成长,共同发展。
归因于成熟的基建,业务研发人员更需关心的是数据库设计方案、操作数据时的性能和一致性问题。如果我们在使用事务时,不知道数据存储方式和事务实现原理,往往会在一个事务的多次读写过程中产生bug,即数据的变更不符合预期。因此当了解了MySQL事务的底层实现原理,我们就能知道如何编写代码以达到预期,就能知道数据库引擎设计的精妙之处。
详细介绍MySQL InnoDB的数据模型、数据持久化策略、事务提交以及故障恢复原理。
InnoDB逻辑存储结构层级:表空间->段->区->页->行
如上图所示,数据表有许多数据行,分别存储在16KB的Page上,把一定数量的Page整合为了一个Extent(默认是64个Page即共1M),而多个Extent又构成了一个Segment,不同类型的Segment又组成了对应类型的表空间。
InnoDB总体结构分为内存结构(下图左侧)和磁盘结构(右侧)两部分。
磁盘部分包括各种表空间,包括系统表空间(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等依然存放在系统表空间。
多个段组成一个表空间。常见的段有数据段、索引段、回滚段等,段是一个逻辑的概念,是一些零散页面和一些完整的区的集合。不同类型的数据保存在单独的段内,可以更好的保持该类型数据的连续性,可以提升访问磁盘的效率。创建一个索引会创建数据段和索引段,即一个索引占用两个段。
注意,虽然InnoDB区分了数据段和索引段,但由于数据是以主键为索引来组织数据的存储的,所以索引文件和数据文件都在同一个文件中,都在“.ibd”文件里面。
表空间中的页实在是太多了,为了更好的管理这些页面,InnoDB提出了区的概念。一个表空间划分为多个区(extent),一个区内包含物理上连续的64个页,因此一个区空间大小为64*16KB=1M。区就是为了保证页的连续性,InnoDB一次会从磁盘申请4~5个区。
段可以简单理解为是一个逻辑的概念,而Extent是一个物理概念,每次B+树的扩容都是以Extent为单位来扩容的,默认一次扩容不超过4个Extent。
段区分了数据段和索引段,其实也就有了各自的区,即叶子节点和非叶子节点都有自己独立的区。想象一下,当B+树按顺序范围查询时,如果数据分布在磁盘的不同位置,就会产生随机IO,而如果数据的物理位置相邻,就可以通过顺序IO读取了。
页是InnoDB中管理数据的最小单元,是固定大小的一段连续磁盘空间,默认为16KB,用于存放数据、索引等各种类型的数据。
InnoDB中,常见的页类型有数据索引页、undo page、文件管理页FSP_HDR/XDES、插入缓冲IBUF_BITMAP页、INODE页等。
在InnoDB中的设计中,页与页之间是通过一个双向链表连接起来,而存储在页中的数据行则是通过单链表连接起来的,如下图:
页有通用的文件头和尾(将页的内容进行封装,通过文件头和文件尾的checksum方式来确保页的完整性),但是中部的内容根据页的类型不同而发生变化。我们主要关注数据页和索引页,这种类型的页包括七个部分:
数据行即一行一行的数据。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的变长字段长度列表,这种方式存储数据会更加紧凑(页中存放的行数越多,性能就越高),数据布局如下图:
注意,索引也是按这种方式存储的:
buffer pool是InnoDB的缓存,用来存放各种数据,包括索引页(index page)、数据页(data page)、undo页、插入缓冲、自适应哈希索引(AHI)、innodb存储的锁信息、数据字典等。把磁盘上的数据加载到缓冲池中(通过预读机制加载当前页、相邻页),可避免每次访问都进行磁盘IO,起到加速访问的作用。应用程序在对数据库执行增删改操作的时候,实际上主要都是针对内存里的buffer pool中的数据进行的。
buffer pool包含三种数据类型:
针对这3种页,InnoDB使用3种链表维护:
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来保存数量操作记录从而解决此问题,见下文。
change buffer(写缓存)是一种特殊的数据结构,可以避免数据更改时因为隐式查询数据带来的磁盘IO。change buffer默认占buffer pool的 25%,最大允许占50%。可以根据写业务的量调整,写操作越频繁,change buffer带来的性能提升越明显。
change buffer工作原理如下:
综上:change buffer适合写多读少的场景,并且满足非唯一索引。
Adaptive Hash Index(AHI,自适应哈希索引),是指InnoDB存储引擎通过监控表上索引页的查找模式,自动根据查找模式对“热点数据”来创建哈希索引。因为对B+树索引的访问需要依次访问根节点>中间节点>叶子节点,而对哈希索引的访问仅需要一次HASH计算即可定位到目标位置。一些资料统计,启用AHI后,读取和写入速度可以提高2倍,辅助索引的连接操作性能可以提高5倍。
通过下列指令可以查询到数据库的相关设置:
show variables like '%hash_index';(DMS设置的是OFF)
AHI使用条件:
AHI使用buffer pool中的数据页进行构造,仅保存在内存中,且仅对热点数据进行处理,因此构造AHI速度极快。
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的介绍。
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的性能,InnoDB将各个事务的各种操作通过上文提到的undo segment分开存储(undo segment的undo page通过链式存储,即每个事务都有自己的insert undo链表、update undo链表),而每个段的第一个undo page通过TRX_UNDO_STATE属性存储了该段的一些事务信息,取值有下面几个:
在事务未提交前TRX_UNDO_STATE是TRX_UNDO_PREPARED状态,事务提交后,根据不同的操作类型转换成TRX_UNDO_CACHED、TRX_UNDO_TO_FREE或者TRX_UNDO_TO_PURGE状态,表示满足一定条件后可以清理这些undolog,事务如果需要回滚的话,必须是TRX_UNDO_ACTIVE或者TRX_UNDO_PREPARED状态,故事务的提交是由该属性判断的,详情见下文的事务执行流程。
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会相互穿插。
binlog是属于MySQL Server层面的,又称为归档日志,属于逻辑日志,是以二进制的形式记录的,是sql语句的原始逻辑,主要是用于进行集群中保证主从一致以及执行异常操作后恢复数据。
binlog日志文件默认大小由磁盘决定,顺序追加写入。binlog内存数据存储在binlog cache中(大小由binlog_cache_size控制),磁盘数据则存储在binlog file中。
binlog有三种格式,分别是Row、Statement、Mixed。
binlog和redolog虽然都保存了记录的修改日志,但两者有一些区别:
InnoDB内存部分包括缓冲池(buffer pool) 和日志缓冲(log buffer),两者刷盘方式不同,前者走direct_io模式(直接绕过Page Cache来访问磁盘),后者走Page Cache模式(IO操作需要委托操作系统来完成)。
OS的Page Cache对读写做了不少优化,包括按顺序预读取(按页读取)、在成簇磁盘块(n次方个扇区)上执行IO、允许访问同一文件的多个进程共享高速缓存的缓冲区等,但数据必须在用户进程与内核互相拷贝。
direct_io的优点是减少操作系统缓冲区和用户地址空间的拷贝次数,降低了CPU和内存带宽的开销。而InnoDB本身也已处理好buffer pool与磁盘数据的对应关系,所以可以舍去Page Cache。
先写日志,再写磁盘(WAL机制,Write-Ahead Logging),即redolog和binlog等日志数据刷盘到log文件完成后,才会将脏页从buffer pool刷盘到表文件。
为什么运用WAL机制?因为顺序写磁盘的性能堪比写内存,所以写日志会比数据刷盘的性能高很多,只要保证日志写入成功,再通过代码保证日志和需刷盘数据的一致性,就能在保证数据不丢失的情况下大大提高性能。
顺序写运用很广泛,比如kafka追加写实现了事务消息,即提交或回滚事务时,会追加写入一条控制类型的消息来标识是commit或rollback。
buffer pool刷盘时机主要有以下四种:
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)。
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等。
下图展示了一组4个文件的redolog日志,checkpoint_lsn之前的空间表示可以进行写的文件。
我们再看下log buffer刷盘的具体过程:
如果数据库停机,那么第三步之后操作系统可以保证数据写入磁盘;如果是操作系统停机,此时磁盘也无法正常工作,那就必须完成这五步才能保证数据落盘。
如上所述,在将写操作写入redolog的过程中也不是直接就进行磁盘IO来完成的,而是分为三个步骤:
InnoDB_flush_log_at_trx_commit参数控制了log buffer的刷盘时机(值可为0、1、2,默认1):
除此之外,当log buffer空间不足、做checkpoint、Mysql正常关闭、binlog切换等情况也会触发redolog刷盘。刷盘操作是异步IO,由专门的线程完成这件事,不会阻塞用户请求的处理。redolog如果没有及时刷盘或者只刷盘一部分,是会导致事务丢失的。
InnoDB的undolog严格的讲不是Log,而是数据,因此他的管理和落盘都跟数据一样:
之所以这样实现,首要的原因是undolog需要承担MVCC对历史版本的管理作用,设计目标是高事务并发,方便的管理和维护,因此当做数据更合适。
binlog也有独立的刷盘策略,通过sync_binlog参数控制(值分别为0、1、N,默认为1):
由于binlog是属于MySQL Server层面的日志,只需追加写入即可。
有一个名叫X/Open的组织提出了一个名为XA的规范。这个XA规范提出了2个角色:
要提交一个全局事务,那么属于该全局事务的若干个小事务就应该全部提交,只要有任何一个小事务无法提交,那么整个全局事务就应该全部回滚。XA规范中指出,要提交一个全局事务,必须分为2步:
XA规范把上述全局事务提交时所经历的两个阶段称作两阶段提交。在单个MySQL实例中,将server层作为事务协调器,存储引擎作为事务管理器,故本文将binlog作为事务协调器。
sql提交到MySQL时需要进行词法语法分析、优化(如果没有命中索引,就会扫全表),才会执行:
假设我们要更新一条数据,语句如下:
update T set c=c+1 where ID=2;
redolog未写入,binlog未写入:此时MySQL异常重启无法恢复数据,认为sql就没执行。
redolog写入,binlog未写入:此时MySQL异常重启能根据redolog恢复事务提交时的数据,但binlog没有记录,后续使用binlog恢复临时库会出现数据丟失,导致状态不一致。
binlog写入,redolog未写入:此时MySQL异常重启临时库能根据binlog重放事务提交时的数据,但redolog没有记录,如果主库有一些脏页已经刷盘,本应先回滚再通过binlog重放,但现在无法回滚,会导致状态不一致。
所谓两阶段提交,就是指同时将redolog和binlog都写成功,这样既能保证通过binlog恢复临时库时和主库无差异,又能保证通过redolog恢复主库时和临时库无差异。
本文标题:InnoDB数据存储及事务两阶段提交原理解析
网站链接:http://www.36103.cn/qtweb/news36/8986.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联