面试题:MySQL要点总结(进阶)

发布于:2025-09-10 ⋅ 阅读:(19) ⋅ 点赞:(0)

一.InnoDB的内存结构

1.Buffer Pool的概念

2.Buffer Pool的Page管理机制

3.Change Buffer的概念和作用

4.Log Buffer的概念和作用

在这里插入图片描述

1.Buffer Pool的概念

(1)Buffer Pool基本概念

Buffer Pool是缓冲池的意思。Buffer Pool的作用是缓存表数据与索引数据,减少磁盘IO,提升效率。Buffer Pool由缓存数据页(Page)和对缓存数据页进行描述的控制块(描述数据块)组成。控制块(描述数据块)存储着缓存页的表空间、数据页号、在Buffer Pool中的地址等。Buffer Pool默认大小是128M,以Page页为单位,Page页默认大小16K。而控制块的大小约为数据页的5%,大概是800字节。

(2)如何判断数据页是否缓存在Buffer Pool

MySQl中有一个哈希表数据结构:key是表空间号 + 数据页号,然后value就是缓存页对应的控制块。当需要访问某个页的数据时:会先从哈希表中根据表空间号 + 数据页号看看是否存在对应的缓存页。如果有,则直接使用。如果没有,则从Free链表中选出一个空闲的缓存页,然后把磁盘中对应的页加载到该缓存页的位置。

2.Buffer Pool的Page管理机制

(1)Page根据状态可以分为三种类型

一.Free Page:空闲Page,未被使用

二.Clean Page:被使用Page,没被修改

三.Dirty Page:被使用Page,已被修改

(2)Free List表示空闲缓冲区(管理Free Page)

Buffer Pool初始化时会先向操作系统申请连续的内存空间,然后把它划分成若干个控制块&缓存页,接着把所有空闲的缓存页对应的控制块作为节点放到一个链表中,这个链表就是Free链表。

从磁盘加载数据页的流程:

首先从Free链表中取出一个空闲的控制块(对应缓存页)。然后把该缓存页对应的控制块信息填上,如缓存页所在的表空间、数据页号之类的信息。接着把该缓存页对应的Free链表节点(即控制块)从链表中移除,表示该缓存页已经被使用了。

(3)Flush List表示需要刷新到磁盘的缓冲区(管理Dirty Page)

Flush List管理的Dirty Page会按修改时间排序。InnoDB引擎为了提高处理效率,在每次修改缓存页后,并非立刻把修改刷新到磁盘上,而是在未来某个时间点进行刷新操作。凡是被修改过的缓存页对应的控制块都会作为节点加入到Flush链表,Flush链表的结构与Free链表的结构相似。

脏页既存在于Flush链表中,也存在于LRU链表中,但它们互不影响。LRU链表负责管理Page的可用性和释放,Flush链表负责管理脏页的刷盘操作

(4)LRU List表示正在使用的缓冲区(管理Clean Page和Dirty Page)

缓冲区以midpoint为基点:链表的前部分称为热数据列表,存放经常访问的数据,占63%。链表的后部分称为冷数据列表,存放使用较少数据,占37%。

3.Change Buffer的概念和作用

(1)Change Buffer基本概念

Change Buffer是写缓冲区,用于优化对二级索引(辅助索引)页的更新。对于DML操作,如果请求的是辅助索引(非唯一键索引)且还没有在Buffer Pool中,那么不会立刻将数据页加载到Buffer Pool中,而是会先在Change Buffer中记录数据的变更,等未来数据被读取时再将数据合并恢复放到Buffer Pool,以减少磁盘IO。

(2)Change Buffer的数据更新流程

情况1:对于唯一索引,需要将数据页读入内存。然后判断没有冲突才插入更新值,语句执行结束。

情况2:对于普通索引,更新一条记录的步骤如下:

步骤一:如果该记录在Buffer Pool中存在,那么就直接在Buffer Pool中修改,进行一次内存操作。

步骤二:如果该记录在Buffer Pool中不存在(没有命中),那么在不影响数据一致性的前提下,InnoDB会将该记录的更新操作缓存在Change Buffer中,先不用去磁盘查询数据,从而避免一次磁盘IO。

步骤三:当下次查询该记录时,InnoDB才会将数据页读入内存,然后执行Change Buffer中与该记录有关的操作。

(3)为什么写缓冲区仅适用于二级索引页

如果新增或修改发生在唯一索引中,那么InnoDB必须要做唯一性校验。此时就必须查询磁盘,进行一次IO操作。也就是会直接将记录查询到Buffer Pool中,然后在Buffer Pool修改,不需要在Change Buffer操作了。如果新增或修改发生在非索引中,那么InnoDB还是要做唯一性校验。此时也必须查询磁盘,进行一次IO操作。

(4)Change Buffer的使用场景

Change Buffer的主要目的是将记录的变更操作缓存下来,所以在merge发生前应当尽可能多的缓存变更信息,这样Change Buffer的优势发挥得就越明显。应用场景是写多读少的业务,此时页面在写完后马上被访问的概率较小,Change Buffer使用效果最好,这种业务模型常见的就是账单类、日志类的系统。

4.Log Buffer的概念和作用

(1)Log Buffer的概念和作用

Log Buffer是用来保存要写入磁盘log文件的log数据。Log Buffer可以优化每次更新操作后要写文件而产生的磁盘IO问题,因为每次更新操作都是需要写log到redo log和undo log磁盘文件的。

Log Buffer日志缓冲区的内容会定期刷新到磁盘log文件中,Log Buffer日志缓冲区满时会自动将其刷新到磁盘。当遇到BLOB或多行更新的大事务时,增加日志缓冲区可节省磁盘IO。

Log Buffer主要是用于记录InnoDB引擎日志,InnoDB在DML操作时会产生redo和undo日志。Log Buffer空间满了,会自动写入磁盘,默认16M。可以通过将innodb_log_buffer_size参数调大,减少磁盘IO频率。

(2)Adaptive Hash Index介绍

自适应哈希索引,用于优化对Buffer Pool数据的查询,InnoDB存储引擎会监控对表索引的查找。

自适应哈希索引指的是:如果观察到建立哈希索引可以带来速度的提升,则建立哈希索引。InnoDB存储引擎会自动根据访问的频率和模式来为某些页建立哈希索引。

二.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

表空间分为:系统表空间、临时表空间、常规表空间、Undo表空间及独立表空间。系统表空间又包括:Change Buffer(写缓冲区)、Double Write Buffer(双写缓冲区)等。

(2)Double Write Buffer

InnoDB将数据页写到文件前存放的位置。

(3)redo log

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

(4)undo log

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

2.表空间(Tablespaces)

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

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

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

通用表空间和系统表空间类似,也是共享的表空间,一个通用表空间文件能够存储多个表的数据。

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

临时表空间用来保存用户创建的临时表和磁盘内部的临时表。

3.数据字典(Data Dictionary)

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

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

(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数据页的可靠性。

(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的写入机制

(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的写入机制

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。日志组中每个重做日志文件的大小是一致的,都是48M,并循环使用。

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

WritePos指针表示日志当前记录的位置,当ib_logfile_1写满后,会从ib_logfile_0从头开始记录。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的存储机制

(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记录的是逻辑日志。当insert一条记录时,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万个并发事务执行。

7.二进制日志(binlog)

(1)binlog的基本概念

(2)binlog落盘策略

(3)redo log和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落盘策略

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

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

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

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

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

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

(3)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服务器宕机重启后,能够保证:已提交的事务的数据仍然存在 + 没提交的事务的数据自动回滚。

8.表空间文件结构

InnoDB表空间文件结构从逻辑上可以分为:

Tablespace(表空间)->Segment(段)->

Extent(区)->Page(页)->Row(行)

一个表空间会包含多个段,一个段会包含多个区(256个区就是一个组)。一个区又会包含64个页,一个页里面又会包含一行一行的记录Row。

(1)表空间Tablesapce

一.表空间是什么

表空间能够看作是InnoDB存储引擎逻辑结构的最高层。表空间用于存储多个ibd数据文件,用于存储表的记录和索引。一个表空间文件可以包含多个段:叶子节点段、非叶子节点段、回滚段。

二.表空间类型

系统表空间、独占表空间、通用表空间、 临时表空间、Undo表空间。

(2)段Segment

一.段是什么

段是用来管理空间的申请以及将同类的区和页用链表管理起来。段是个逻辑概念,本质上是由若干个零散页面和若干个完整的区组成。段是为了保持叶子节点在磁盘上的连续,可以实现更好的顺序IO操作。

一个B+树索引被划分为两个段:一个叶子节点段和一个非叶子节点段。这样叶子节点就可以尽可能地存放在一起,非叶子节点也可以尽可能地存放在一起。

二.段的类型

常见的段有数据段、索引段、回滚段等。其中索引段就是非叶子节点部分,而数据段就是叶子节点部分,回滚段用于数据的回滚和多版本控制。

三.为什么引入段

原因一:

使用B+树执行查询时只是扫描叶子节点记录,如果不区分叶子节点和非叶子节点,通通把节点代表的页面放到申请的区中,那么扫描效果就大打折扣,而段可以让叶子节点的数据页尽可能连续和差距不那么大。所以InnoDB对B+树的叶子节点和非叶子节点进行区别对待,叶子节点和非叶子节点各有自己独有的区。而存放叶子节点的区的集合就算是一个段,存放非叶子节点的区的集合也算是一个段。即一个索引会生成两个段:一个叶子节点段和一个非叶子节点段。

原因二:

以完整的区为单位分配给某个段时,对于数据量较小的表来说太浪费存储空间。因为当段以区为单位申请存储空间时,由于一个区默认占用1MB存储空间以及一个聚簇索引会生成两个段,所以默认情况下只存放了几条记录的小表也需要2MB的存储空间,这就有点浪费了。

原因三:

出现上述问题的根源是:区中的所有页面都是为了存储同一个段的数据而存在,即使区的页面用不完也不能作他用。于是InnoDB便有了碎片区。在一个碎片区中,并非所有页都是为了存储同一个段的数据而存在,碎片区的页可以用于不同的目的的。比如有些页属于段A、有些页属于段B、有些页甚至不属于任何段。碎片区直属于表空间,不属于任何段。

原因四:

为某个段分配存储空间的策略:刚开始向表中插入数据时,段是从某个碎片区中以单个页面为单位来分配存储空间的。当某个段已经占用了32个碎片区页面后,就会以完整的区为单位来分配存储空间,原先占用的碎片区页面并不会被复制到新申请的完整的区中。所以说,段是一些零散的页面以及一些完整的区的集合。

(3)区Extend

一.区是什么

区由连续页组成的空间,一个区的大小是1M,一个区有64个连续的页。为了保证区中页的连续性,扩展时InnoDB一次从磁盘申请4~5个区。无论是系统表空间还是独立表空间,都可看成是由若干个连续的区组成。当一个段使用了32个碎片页后才是以区来分配,每256个区被分成一组。

二.为什么引入区

原因一:

向表中插入一条记录,本质上就是向该表的聚簇索引以及所有二级索引代表的B+树的节点中插入数据。而B+树每一层中的页都会形成一个双向链表,如果以页为单位来分配存储空间,那么双向链表中相邻的两个页之间的物理位置可能离得非常远。

原因二:

使用B+树来减少记录的扫描行数的过程是:通过一些搜索条件,到B+树的叶子节点中定位到第一条符合该条件的记录,然后沿着由记录组成的单向链表以及由数据页组成的双向链表,一直向后进行扫描。全表扫描就是定位到第一个叶子节点的第一条记录。

原因三:

如果双向链表中相邻的两个页的物理位置不连续,对于传统的机械硬盘来说,需要重新定位磁头位置,也就是会产生随机IO,影响性能。所以应尽量让页面链表中相邻的页的物理位置也相邻,以便扫描叶子节点的大量记录时可以使用顺序IO。

原因四:

为了尽量消除随机IO才引入了区的概念,一个区就是物理位置上连续的64个页,区中页面的页号都是连续的。当表中的数据量很大时,为某个索引分配空间时就不再按页为单位来分配了,而是按照区为单位进行分配。甚至当表中的数据非常非常多的时候,可以一次性分配多个连续的区,以消除更多的随机IO,但会造成一点空间的浪费。

(4)页Page

一.页是什么

区是由连续的页(Page)组成的空间,一个页的存储大小为16K,页用于存储多个Row行记录。

二.页的类型

页有很多种类型,如数据页、Undo页、系统页、事务数据页、大的BLOB对象页。

(5)行Row

InnoDB的数据是按行的方式进行存放的,每个页存放的行记录最多允许存放16K / 2 - 200行的记录,即每个页最多存放7992行记录。每行记录根据不同的行格式、不同的数据类型,会有不同的存储方式。

行记录包含的内容:记录的字段值、事务ID、回滚指针等信息。如果行记录中没有主键列,则MySQL会为其生成隐藏的主键列。步骤如下:

步骤一:服务器会在内存中维护一个全局变量,每当向某个包含隐藏的row_id列的表中插入一条记录时,就会把该变量的值当作新记录的row_id列的值,并且把该变量自增1。

步骤二:每当这个变量的值为256的倍数时,就会将该变量的值,刷新到系统表空间的页号为7的页面中一个Max Row ID的属性中。

步骤三:系统启动时会将页中的Max Row ID属性加载到内存中,并将该值加上256后赋值给全局变量,因为在上次关机时该全局变量的值可能大于页中Max Row ID属性值。

三.索引优化

1.Explain的字段说明

2.索引优化原则总结

3.慢查询优化思路

1.Explain的字段说明

(1)Explain的type字段说明

type字段表示的是连接类型,描述了找到所需数据而使用的扫描方式。可以只关注以下几种连接类型 :system -> const -> eq_ref -> ref -> range -> index -> ALL。一般来说,需要保证查询至少达到range级别,最好能达到ref级别,否则就要就行SQL的优化调整。

//system
//不进行磁盘IO,查询系统表,仅仅返回一条数据。

//const
//查找主键索引,最多返回1条或0条数据,属于精确查找。

//eq_ref
//查找唯一性索引,返回数据最多一条,属于精确查找。

//ref
//查找非唯一性索引,返回匹配的多条数据,属于精确查找。

//range
//查找索引中给定范围的行,属于范围查找(>、<、in、between)。

//index
//使用了索引但扫描全部了,比all快,因索引文件比数据文件小。

//index
//比如遍历二级索引不需要回表或者主键排序全表扫描。

//all
//不使用任何索引,直接进行全表扫描。
(2)Explain的possible_keys字段说明

表示可能用于查询的表上的索引。查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询实际使用。

(3)Explain的key字段说明

表示实际使用的索引。若为null,则表示没有使用到索引或索引失效。查询中若使用了覆盖索引,则该索引仅出现在key列表中。

(4)Explain的key_len字段说明

表示索引中使用的字节数,通过该列可以计算查询中使用索引的长度。key_len字段能够帮我们检查是否充分利用了索引,ken_len越长越好,说明索引使用的越充分。

(5)Explain的ref字段说明

表示的是显示索引的哪一列被使用了,如果可能的话,最好是一个常数。表示的是哪些列或常量被用于查找索引列上的值。

(6)Explain的rows字段说明

表示MySQL为了找到所需的记录,一共访问了多少行(预估的)。需要注意的是rows只是一个估算值,并不准确。所以rows行数过大的问题并不值得过多考虑,主要分析的还是索引是否使用正确了。

(7)Explain的filtered字段说明

它指返回结果的行占需要读到的行(rows列的值)的百分比。

(8)Explain的extra字段说明

Extra是EXPLAIN输出中另外一个很重要的列,该列显示MySQL在查询过程中的一些详细信息。

//using index
//查找时使用了索引覆盖时就会出现,不需要回表。

//using where
//查找时使用索引的情况下需要回表或全表扫描。

//using index condition
//查找时使用了索引但只用一部分索引,需要回表。

//Using filesort
//在一个没有建立索引的列上order by,发生文件排序。

//Using temporary
//使用了临时表存储结果集,常见于排序和分组查询。

//当遍历二级索引不需要回表或者主键排序全表扫描时,type就为index。
//查找非唯一性索引,返回匹配的多条数据,type就为ref。
//查找唯一性索引,返回匹配的数据最多一条,type就为eq_ref。
//查找索引中给定范围的行,type就为range。

2.索引优化原则总结

最左侧列匹配和最左前缀匹配

索引列上不计算不转换

范围之后全失效

最好使用覆盖索引

!=、is null、is not null、or会索引失效

like百分号加右边,加左边导致索引失效的解决方法是使用覆盖索引

3.慢查询优化思路

(1)优先选择优化高并发执行的SQL

因为高并发的SQL出现问题带来后果更严重,比如下面两种情况:SQL1每小时执行10000次,每次20个IO,优化后每次18个IO,每小时节省2万次IO。SQL2每小时10次,每次20000个IO,每次优化减少2000个IO,每小时节省2万次IO。此时SQL2更难优化,SQL1更好优化。但是第一种属于高并发SQL,更急需优化,因为成本更低。

(2)定位优化对象的性能瓶颈

在去优化SQL时,选择优化分方向有三个:

方向1:IO,数据访问消耗了太多时间,查看是否正确使用索引。

方向2:CPU,数据运算花费了太多时间,数据的运算分组、排序是不是有问题。

方向3:网络带宽,加大网络带宽。

(3)明确优化目标

根据数据库当前状态、当前SQL的具体功能,来确定最好情况下消耗的资源和最差情况下消耗的资源。因为优化的结果只有一个,即给用户一个好的体验。

(4)从explain执行计划入手

只有explain能告诉我们当前SQL的执行状态。

(5)永远用小的结果集驱动大的结果集

小的数据集驱动大的数据集,减少内层表读取次数。

(6)尽可能在索引中完成排序

排序操作用得比较多,所以order by后面的字段尽量使用上索引。因为索引本来就是排好序的,所以速度很快。没有索引的话,就需要从表中拿数据,在内存中进行排序。如果内存空间不够还会发生临时文件落盘操作。

(7)只获取自己需要的列

不要使用select *,因为select * 很可能不使用索引,而且数据量过大。

(8)只使用最有效的过滤条件

where后面的条件并非越多越好,应该用最短的路径访问到数据。

(9)尽可能避免复杂的join和子查询

每条SQL的JOIN操作建议不要超过三张表。将复杂的SQL,拆分成多个小的SQL,单个表执行,然后对获取的结果在程序中进行封装。因为如果join占用的资源比较多,会导致其他进程等待时间变长。

(10)合理设计并利用索引

也就是合理判断是否需要创建索引,以及合理选择合适索引。

四.事务原理

1.ACID之原子性

2.ACID之持久性

3.ACID之隔离性

4.ACID之一致性

5.ACID的关系

6.MVCC事务控制

1.ACID之原子性

原子性:指的是事务是一个原子操作单元,它对数据的修改,要么全执行要么全不执行。

每写一个事务都会修改Buffer Pool,产生Redo、Undo日志。如果事务提交后redo log已刷入磁盘,此时机器恰好宕机了,那么就可以根据redo log恢复事务修改过的缓存数据。如果需要回滚事务,那么就可以基于undo log进行回滚,也就是通过undo log回滚事务之前对缓存页做的修改。所以undo日志实现了事务的原子性。

2.ACID之持久性

持久性:指的是一个事务一旦提交,它对数据库中数据的改变就应该是永久性的,后续的操作或故障不应该对其有任何影响,数据不会丢失。

MySQL持久性的保证依赖两个日志文件:redo log文件和binlog文件。最开始MySQL是没有InnoDB引擎的,MySQL自带的引擎是MyISAM引擎。但是MyISAM引擎没有CrashSafe能力,因为binlog日志只能用于归档。

如果InnoDB引擎只依靠binlog日志也是没有CrashSafe能力的。因为binlog日志是逻辑日志,不能直接应用到内存的缓存页。而且虽然binlog拥有归档日志,但是没有标志让InnoDB判断哪些数据已经刷盘。比如binlog日志是一条插入语句,如何确定这个插入语句是否已经刷盘成功。

所以InnoDB要通过redo log才能实现CrashSafe能力,redo日志文件中会存储一些信息比如checkpoint_no和checkpoint_lsn等。如果redo日志的LSN值小于checkpoint_lsn,则说明该redo日志已刷盘,其中checkpoint_lsn其实就是数据页的LSN。

(1)update语句的执行流程

步骤一:执行器先找引擎获取ID=2这一行数据。由于ID是主键,引擎直接用树搜索找到这一行。如果ID=2这一行所在的数据页本来就在内存中,就直接返回给执行器。否则,先从磁盘读入内存,然后再返回。

步骤二:执行器拿到引擎给的行数据,把值加1。比如原来是N,现在就是N+1。于是会得到新的一行数据,接着再调用引擎接口写入这行新数据。

步骤三:引擎将新数据更新到Buffer Pool + 记录更新操作到redo log。此时redo log的状态处于prepare状态,引擎会告知执行器已执行完成,随时可以提交事务。

步骤四:执行器生成这个更新操作的binlog,并把binlog写入磁盘。

步骤五:执行器调用引擎的提交事务接口。此时引擎会把redo log的状态修改成commit状态,用来表示更新已经完成。

(2)两阶段提交

将redo log的写入拆成了两个步骤:prepare和commit,这就是两阶段提交2PC。

(3)为什么使用两阶段提交

使用两阶段提交主要是用来解决binlog和redo log的数据一致性问题。由于InnoDB会先写redo log,再写binlog,如果没有两阶段提交,那么在主从架构下,如果主库的redo log写盘了,但是binlog没写,结果主库宕机了,此时只能靠从库恢复数据,那么就会出现数据不一致。

当进行崩溃恢复时,redo log和binlog有一个共同的数据字段,叫XID。崩溃恢复时会按顺序扫描redo log,如果碰到既有prepare又有commit的redo log,就直接提交。如果redo log处于prepare,则拿着XID去binlog判断对应binlog是否完整。如果binlog完整,则提交事务,如果binlog不完整,则回滚事务

其实崩溃恢复只有三种情况:

情况一:在写入redo log之前崩溃。此时redo log和binlog都没有写,数据是一致的,崩溃也无所谓。

情况二:在写入redo log的prepare阶段后马上崩溃。那么在恢复时,由于redo log没有被标记为commit,于是会拿着redo log中的XID去binglog中查找,此时查不到便回滚。

情况三:在写入binlog后崩溃。那么在恢复时,拿着redo log中的XID就能找到对应binglog直接提交。

(4)持久性的保证

redo log在系统崩溃时,可修复数据,从而保障事务数据的持久性。

一.通过undo log实现的事务原子性可以保证逻辑上的持久性。

二.通过InnoDB存储引擎的数据刷盘可以保证物理上的持久性。

3.ACID之隔离性

隔离性:指的是一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对其他的并发事务是隔离的。

不考虑隔离性可能会引发如下问题:

一.脏读:一个事务读取到了另一个事务修改但未提交的数据。未提交的数据后面回滚了,造成了使用到脏数据。

二.不可重复读:一个事务中多次读取同一行记录的结果不一致。后面读取的与前面读取的结果不一致。

三.幻读:一个事务中多次按相同条件查询的结果不一致。后续查询的结果和面前查询结果不同,多了或少了几行记录。

InnoDB支持的隔离性有4种,隔离性从低到高分别为:读未提交、读已提交、可重复读、可串行化。而通过锁(解决脏写) + MVCC(解决脏读 + 不可重复读 + 幻读)可以保障事务数据的隔离性。

4.ACID之一致性

一致性:指的是事务开始前和事务结束后,数据库的完整性限制未被破坏。原子性、持久性、隔离性共同保证了数据的一致性。

一致性包括约束一致性和数据一致性。

一.约束一致性

创建表结构时所指定的外键、Check、唯一索引等约束,不过在MySQL中并不支持Check。

二.数据一致性

这是一个综合性的规定,因为它是由原子性、持久性、隔离性共同保证的结果,而不是单单依赖于某一种技术。

一致性可以理解为数据的完整性,数据的完整性是通过原子性、隔离性、持久性来保证的,而这3特性又是通过Redo/Undo来保证的。

一致性也可以理解为业务逻辑的一致性,业务逻辑上的一致性包括唯一索引、外键约束、check约束。

5.ACID的关系

事务的持久化是为了应对系统崩溃时造成的数据丢失问题,只有保证了事务的一致性,才能保证执行结果的正确性。

在非并发状态下,事务间天然保证隔离性。所以在非并发状态下,只需要保证事务的原子性即可保证一致性。在并发状态下,则需要严格保证事务的原子性、隔离性才能保证一致性。

6.MVCC事务控制

(1)MVCC概念

MVCC(Multi Version Concurrency Control)被称为多版本并发控制。指的是在数据库中为实现高并发的数据访问,对数据进行多版本处理,并通过事务的可见性保证事务能看到自己应该看到的数据版本

MVCC最大的好处是读不加锁,读写不冲突。在读多写少的系统应用中,读写不冲突能极大的提升系统的并发性能,这也是为什么现阶段几乎所有的关系型数据库都支持MVCC的原因。不过目前MVCC只在RC和RR两种隔离级别下工作。

(2)undo log多版本链

每条数据都有两个隐藏字段:事务ID(trx_id)和回滚指针(roll_pointer)。

事务ID(trx_id)表示最近一次更新这条数据的事务ID,回滚指针(roll_pointer)表示指向之前生成的undo log。当然可能还会有一个隐藏字段叫row_id,但没有主键ID时才会自动生成。

下面演示undo log多版本链的生成:

时间点1:事务A插入数据

假设有一个事务A(trx_id=50),向表中插入一条数据。插入的这条数据的值为A,其中roll_pointer会指向一个空的undo log

时间点2:事务B修改事务A插入的数据

接着有一个事务B(trx_id=58)对事务A插入的数据进行修改,将值改为B。事务B的ID是58,在更新前会生成一个undo log来记录之前的值,然后让roll_pointer指向这个事务B生成的undo log。

时间点3:事务C更新事务B修改后的数据

如果再有一个事务C(trx_id=69)继续更新该条记录值为C,则会跟时间点2的步骤一样。

总结:每一条数据都有多个版本,版本之间通过undo log链条进行连接。通过这样的设计,可以保证每个事务提交时一旦需要回滚操作,同一个事务只能读到比当前版本更早提交的值,不能看到更晚提交的值。

(3)ReadView

Read View是InnoDB在实现MVCC时用到的一致性读视图,一致性读视图也就是Consistent Read View。Read View是用来支持RC(读已提交)和RR(可重复读)隔离级别的实现。Read View简单理解就是对数据在每个时刻的状态拍成照片记录了下来,那么之后获取某时刻的数据时还可以是原来照片上的数据,是不会变的。

Read View中比较重要的字段有4个:

字段一:m_ids,用来表示MySQL中哪些事务正在执行但是没有提交。

字段二:min_trx_id,就是m_ids里最小的值。

字段三:max_trx_id,下一个要生成的事务ID值,也就是最大事务ID。

字段四:creator_trx_id,当前事务的ID。

当数据库处于RC隔离级别时,每次执行select都会创建新的Read View,从而保证每次select都能读取到其他事务已经提交的内容。

当数据库处于RR隔离级别时,第一次查询时生成的Read View不会再更新,后续所有查询都复用它。所以能保证每次读取的一致性,也就是都可以读取第一次读取到的内容。可重复读的语义就是防止幻读。

(4)MVCC在MySQL中的具体实现

MySQL实现MVCC机制的方式:undo log多版本链 + Read View机制。在MySQL中实现MVCC时,会为每一个行记录添加如下几个隐藏的字段。

字段一:6字节的DATA_TRX_ID

DATA_TRX_ID标记了最新更新这条行记录的transaction id,每处理一个事务,其值自动设置为当前事务ID,DATA_TRX_ID只有在事务提交之后才会更新。

字段二:7字节的DATA_ROLL_PTR

一个rollback指针,指向当前这一行数据的上一个版本。通过这个指针可以找到之前版本的数据,这个指针会将数据的多个版本连接在一起构成一个undo log版本链。

字段三:6字节的DB_ROW_ID

隐含的自增ID,这是一个用来唯一标识每一行的字段。如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引。

字段四:DELETE BIT位

标识当前记录是否被删除,不是真正的删除数据,而是标志出来要删除,真正意义的删除是在commit时执行的。

(6)MVCC读操作分类

在MVCC并发控制中,读操作可以分为两类:快照读(Snapshot Read)与当前读(Current Read)。

快照读:指读取数据时不读取最新版本的数据,而是基于历史版本读取快照信息,比如InnoDB会读取undo log历史版本。快照读可以使普通的SELECT读取数据时不用对表数据进行加锁,从而解决了因为对数据库表的加锁而导致的两个如下问题。问题1是因加锁导致的修改数据时无法对数据进行读取的问题。问题2是因加锁导致的读取数据时无法对数据进行修改的问题。

当前读:是读取的数据库最新的数据,当前读和快照读不同,因为要读取最新的数据而且要保证事务的隔离性,所以当前读是需要对数据进行加锁的。比如以下语句就是当前读:update、delete、insert、select … lock in share mode、select for update。

MVCC已经实现了读读、读写、写读的并发冲突,如果想进一步解决写写冲突,可采用这两种方案:乐观锁、悲观锁。


网站公告

今日签到

点亮在社区的每一天
去签到