概述
这是一份读书笔记,之前一直写在本地没有上传到博客上,同步一下也方便自己。
第1章 MYSQL体系结构和存储引擎
数据库和实例:
数据库
:物理操作系统文件或者其他形式文件类型的集合。在Mysql中,数据库文件可以是frm、ibd、MYD等形式的文件。实例
:Mysql数据库由后台线程以及一个共享内存区组成,而共享内存区可以被运行的后台线程共享。(数据库实例才是真正用于操作数据库文件的角色)
从概念上来说,数据库是文件的集合;数据库实例则是程序,是位于用户与操作系统之间的一层数据管理软件,所有对数据库的操作都要通过这层抽象层进行
MySQL数据库实例在系统上的表现就是一个进程,用户对数据库数据的任何操作,包括数据库定义、数据查询、数据维护、数据库运行控制等都是在数据库实例下进行的,应用程序只有通过数据库实例才能和数据库打交道。
MySQL具体的体系结构可以分为下面几种:
- 连接池:管理、缓冲用户的连接,线程处理等需要缓存的需求。
- 管理服务和工具组件:系统管理和控制工具,例如备份恢复、Mysql复制、集群等 。
- sql接口:接受用户的SQL命令,并且返回用户需要查询的结果。
- 查询解析器:SQL命令传递到解析器的时候会被解析器验证和解析。(权限、语法结构)
- 查询优化器:SQL语句在查询之前会使用查询优化器对查询进行优化。
- 缓存:如果查询缓存有命中的查询结果,查询语句就可以直接去查询缓存中取数据。
- 插入式存储引擎:存储引擎说白了就是如何管理操作数据(存储数据、如何更新、查询数据等)的一种方法。因为在关系数据库中数据的存储是以表的形式存储的,所以存储引擎也可以称为表类型。(即存储和操作此表的类型)
- 物理文件
MySQL区别于其他数据的最重要的特点就是其插件式的表存储引擎,这个表存储引擎是基于表,而不是数据库,即进行存储操作等都是通过record等来进行操作。在你实际使用的过程中,存储引擎是透明的。
InnoDB引擎
InnoDB是第一个完整支持ACID事务的Mysql存储引擎(DBD是第一个支持事务的InnoDB存储引擎,但是注意前面还带了个ACID!别搞混了)
- InnoDB存储引擎支持事务,其特点是行锁设计、支持外键、支持非锁定锁(即默认读取操作不会产生锁)。并且Mysql5.5.8版本起,默认的存储引擎就是InnoDB了。
- InnoDB通过使用多版本并发控制MVCC来获得高并发性,并且实现了SQL标准的4种隔离级别,默认是Repeatable级别(重复读)。
- 使用一种被称为next-key的连接锁策略来避免幻读的产生。
- InnoDB还提供了插入缓冲、二次写、自定义哈希索引、预读等4大特性。
- InnoDB引擎采用了聚集的方式,因此每张表的存储都是按照主键的顺序进行存放。
- 如果没有显式的在表中定义主键,那么InnoDB会为每一行生成一个6字节大小的RowId,并以此为主键。(换句话说,无论咋样,一张表必有主键)
拓展:
那么Mysql的事务是怎么实现的?
Mysql的事务的实现即为ACID的实现:
第一:事务的原子性是通过undo log来实现,也就是所谓的回滚操作。 undo log记录了数据被修改之前的信息以及新增、删除的信息。undo log就是通过生成操作相反的sql语句来实现,举几个栗子🌰:
1.若undo log中有新增记录,则生成删除该记录的sql。
2.若undo log中有删除记录,则生成生成该记录的sql。
3.若undo log中有修改记录,则生成修改至原先语句的sql。
因此,所谓的回滚操作就是根据undo log做一个逆向操作。
第二:事务的持久性(这里就说几个重要的点,因为说白了,持久性跟存储有关):
1.redolog在提交commit前会写一次数据,顺序存储。
2.InnoDB的二次写以及自带的buffer pool。
第三:事务的隔离性则通过4种隔离级别来实现。
第四:事务的一致性:其实现依赖于以上3个特性的实现、即回滚、恢复、隔离机制。
(1)存储策略和存储大小
InnoDB存储数据的策略有两种:
- 共享表空间存储方式。
InnoDB的所有数据保存在一个单独的表空间里面,而这个表空间可以由很多歌文件组成,一个表可以跨多个文件存在,所以其大小限制不再是文件大小的限制,而是其自身的限制,官方指出InnoDB表空间的最大限制是64TB。
- 独享表空间存储方式。
每个表的数据以一个单独的文件来存放,此时的单表限制,就变成文件系统的大小限制了。
(2)MVCC和隔离级别
上文提到了InnoDB通过使用多版本并发控制MVCC来获得高并发性,并且实现了SQL标准的4种隔离级别,那接下来就对这两点来进行阐述。
首先MVCC,全名多版本并发控制(Multi-Version Concurrency Control),是Mysql的InnoDB存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。
基本思想: 利用多版本的思想,写操作更新最新的版本快照,而读操作去读旧版本快照,没有互斥关系。(在MVCC中事务的修改操作,delete、insert、update这3个操作会为数据行新增一个版本快照)。
接下来先把4个事务隔离级别的基本概念说清楚(等级从小到大依次排序):
- Read uncommitted(读未提交):避免了更新丢失,却可能出现脏读。(表现:读写并行)
- Read committed(读提交):避免了脏读,但是可能出现不可重复度。(表现:MVCC、读写分离)
- Repeatable read(重复读):避免了不可重复读和脏读,但是有可能出现幻读。(默认的隔离级别,表现:读写锁、MVCC)
- Serializable(序列化,也叫串行化):事务只能一个接着一个的执行,不能并发执行,可以解决幻读问题。
概念解释:
脏读
- A进行了一条数据操作,但是没有提交事务,如果此时B进行这条数据的查询,是可以查到A的数据操作结果的。
- 后来A还没有提交事务,反而不提交了或者进行了事务回滚,那么B查询到的数据就是脏数据。
不可重复读(侧重于修改)
事务A多次读取到同一个数据,而B在A多次读取的过程中,对数据进行了修改,导致事务A多次多去同一个数据的时候,结果不一致。幻读(侧重于增加或者删除)
再一次事务里面,多次查询之后,结果集的个数不一致的情况叫做幻读。而多或者少的那一行数据叫做幻行。
(3)Next-Key Locks
MVCC不能解决幻读的问题,而Next-Key Locks就是为了解决这个问题而存在的。在可重复读级别下,使用MVCC+Next-Key Locks可以解决幻读问题。
一:Record Locks:
锁定一个记录上的索引,而不是记录本身,如果表没有设置索引,由于InnoDB会自动加一个隐藏的主键,因此Record Locks依然可以使用。
记录锁其实很好理解,对表中的记录加锁,叫做记录锁,简称行锁。比如
SELECT * FROM `test` WHERE `id`=1 FOR UPDATE;
它会在 id=1 的记录上加上记录锁,以阻止其他事务插入,更新,删除 id=1 这一行。
需要注意的是:
- id 列必须为唯一索引列或主键列,否则上述语句加的锁就会变成临键锁(有关临键锁下面会讲)。
- 同时查询语句必须为精准匹配(=),不能为 >、<、like等,否则也会退化成临键锁。
其他实现比如,在通过 主键索引 与 唯一索引 对数据行进行 UPDATE 操作时,也会对该行数据加记录锁:
-- id 列为主键列或唯一索引列
UPDATE SET age = 50 WHERE id = 1;
记录锁是锁住记录,锁住索引记录,而不是真正的数据记录.如果要锁的列没有索引,则会进行全表记录加锁,同时记录锁也是排它(X)锁
,所以会阻塞其他事务对其插入、更新、删除。
二:Gap Locks:
锁定索引之间的间隙,但是不包含索引本身。间隙锁是Innodb在RR(可重复读) 隔离级别下为了解决幻读问题
时引入的锁机制。间隙锁是innodb中行锁的一种。使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。
举例来说,假如emp表中只有101条记录,其empid的值分别是1,2,…,100,101,下面的SQL:
SELECT * FROM emp WHERE empid > 100 FOR UPDATE
当我们用条件检索数据,并请求共享或排他锁时,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。这个时候如果你插入empid等于102的数据的,如果那边事物还没有提交,那你就会处于等待状态,无法插入数据。
三:临键锁:
Next-key锁是记录锁和间隙锁的组合,它指的是加在某条记录以及这条记录前面间隙上的锁。即不仅锁定一个记录上的索引,也锁定索引之间的缝隙。他锁定一个前开后闭的区间,例如一个索引包含以下值:10,11,13,20,那么就会锁定这么几个区间:
(-∞,10],(10,11],(11,13],(13,20],(20,+∞)
也可以理解为一种特殊的间隙锁。通过临建锁可以解决幻读
的问题。 每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。
需要强调的一点是,InnoDB 中行级锁是基于索引实现的,临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁
。
四:总结:
这里对 记录锁、间隙锁、临键锁 做一个总结
- InnoDB 中的行锁的实现依赖于索引,一旦某个加锁操作没有使用到索引,那么该锁就会退化为
表锁
。 - 记录锁存在于包括主键索引在内的唯一索引中,锁定单条索引记录。
- 间隙锁存在于非唯一索引中,锁定开区间范围内的一段间隔,它是基于临键锁实现的。
- 临键锁存在于非唯一索引中,该类型的每条记录的索引上都存在这种锁,它是一种特殊的间隙锁,锁定一段左开右闭的索引区间
(4)InnoDB的4大特性
- 插入缓冲(insert buffer):对于非聚簇类索引的插入和更新操作,如果该索引页在缓存中,那么直接插入,先插入到缓冲区中,再以一定的频率和索引页合并。
- 二次写(double write):写数据前,将数据线写入一块独立的物理文件位置(ibdata)然后再写入数据页中。
- 自定义哈希索引(ahi):自定义哈希索引即将字典类型的索引通过哈希函数映射于一张表,让查询的时候更加迅速。
- 预读(read ahead):InnoDB在IO的优化上做出了预读机制,就是发起一个IO请求,异步地在缓冲池中预先回迁若干页面,预计把可能用到的数据页返回。
MyISAM引擎(了解)
和InnoDB引擎相比,MyISAM不支持事务、表锁设计,但是支持全文索引。MyISAM存储引擎的另一个与众不同的地方是他的缓冲池只缓存索引文件而不缓存数据文件。 另外,还记得上文提到,InnoDB的最大存储限度有一个64TB,Mysql5以上的时候,如果用的是MyISAM引擎,那么支持256TB的单表数据。 并且使用MyISAM存储引擎表,MySQL只缓存索引文件,数据文件缓存则是交给操作系统。
MyISAM和InnoDB的区别
比较内容 | MyISAM | InnoDB | |
---|---|---|---|
构成上的区别 | 每个MyISAM在磁盘上存储成三个文件。第一个文件的名字以表的名字开始,扩展名指出文件类型。 | 基于磁盘的资源是InnoDB表空间数据文件和它的日志文件,InnoDB 表的大小只受限于操作系统文件的大小,一般为 2GB | |
是否支持事务 | 不支持 | 支持 | |
支持的锁 | 行锁 | 表锁 | |
是否有MVCC | 不支持 | 支持 | |
是否支持外键 | 不支持 | 支持 | |
是否支持全文索引 | 支持 | 不支持 | |
操作的速度 | 建议如果执行大量的select语句,使用MyISAM | 如果数据执行大量的insert和update操作,出于性能考虑,使用InnoDB表 | |
表的具体行数 | MyISAM会保存好表中的行数,因此对于count操作很快 | InnoDB则没有,需要扫描一遍全表计算 |
一个极具迷惑性的提问:当表的数据量大于1000万时候MySQL的性能会急剧下降吗?
MySQL是数据库,不是文件,随着数据行数增加性能必然会下降,但这些下降不是线性的,如果在对应业务情况下使用合适的存储引擎与配置,那么这些数据量是能承受住的。如在官方手册上所说的InnoDB 1TB数据。
第2章 InnoDB存储引擎
InnoDB体系架构
InnoDB内有多个内存块,这些内存块组成一个大的内存池,负责维护数据的写、访问以及日记的记录,这些内存块由多个后台线程负责处理,来适应不同的任务。
这些后台线程的任务主要用来:
(1)Master Thread
- 负责将缓冲池的数据异步刷新到磁盘
- 保证数据一致性:包括脏页的刷新、合并插入缓冲池、undo页的回收
在后续的更新里,Innodb 1.2.x把刷新脏页从主线程里面分离到了PageCleanerTherad线程中
Master Thread内部由多个循环loop组成,并且在多个循环状态之间切换。
- 主循环(loop)
- 后台循环(background loop)
- 刷新循环(flush loop)
- 暂停循环(suspend loop)
大部分操作在主循环中,分为每秒的操作和每十秒的操作,每秒的操作包括:
1.日志缓冲刷新到磁盘,即使这个事务还没有提交。(总是)
2.合并插入缓冲。(可能)
3.最多刷新100个InnoDB的缓冲池中的脏页到磁盘。(可能)
4.如果当前没有用户活动,则切换到background loop后台循环。(可能)
——-background loop执行的操作:
——-1.删除无用的Undo页。(总是)
——-2.合并20个插入缓冲。(总是)
——-3.跳回到主循环。(总是)
——-4.不断刷新100个页直到符合条件。(可能)
每十秒的操作包括:
1.刷新100个脏页到磁盘。(可能)
2.合并最多5个插入缓冲(总是)
3.将日志缓冲刷新到磁盘。(总是)
4.删除无用的Undo页。(总是)
5.刷新100个或者10个脏页到磁盘。(总是)
(2) IO Thread
InnoDB中使用了大量的AIO来处理写IO请求,以便提高数据库的性能,而IO Thread的工作就是负责这些IO请求的回调处理。 其中比较重要的几个线程为write IO Thread(默认4个)、read IO Thread(默认4个)、insert buffer IO Thread、log IO Thread
(3))Purge Thread
事务被提交后,其所用的undolog可能不再需要,那么则需要一个线程去回收已经使用并分配的undo页。而做这份工作的线程就叫做Purge Thread。
(4)Page Cleaner Thread
Page Cleaner Thread的作用是将之前版本中脏页的刷新操作都放入到单独的线程中去完成。 其目的:减轻原Mster Thread的工作及对于用户查询线程的阻塞,进一步提高InnoDB的性能。
Innodb内存
(1)缓冲池
InnoDB存储引擎是基于磁盘存储的,并且将其中的记录按照页的方式进行管理。 而基于磁盘的数据库系统,因为存储设备速率的差距,通常需要使用缓冲池技术来提高数据库的整体性能,从上图可以看到,缓冲池并不是简单的一个k-v map。
缓冲池作为内存中最大的一块,也包含了很多数据页类型:索引页、数据页、undo页、插入缓冲、自定义哈希索引、锁信息、数据字典信息等。同时InnoDB存储引擎还允许有多个缓冲池实例, 相当于自适应哈希索引中的多个哈希桶。
(2)InnoDB的LRU算法
InnoDB存储引擎则使用了一种经过优化的LRU算法来堆缓冲池进行管理,即在LRU的基础上增加了一个midpoint
的位置,新读取到的页并不会直接放入到LRU列表的首部,而是放到LRU列表的midpoint
位置。默认情况下,这个位置在LRU列表长度的5/8处。即新读取的页给放在LRU列表尾端3/8的位置,midpoint之后的为old列表,之前的为new列表,new列表为频繁访问的数据。
问题1:为什么不采用普通的LRU算法,直接将读取的页放入到LRU列表的首部呢?
如果直接吧读取到的页放入到LRU列表的首部,那么某些SQL操作可能会使缓冲池中的页被刷新出,从而影响缓冲池的效率。
问题2:所谓的会将缓冲池中的页刷新出的SQL某些操作是什么?为什么会影响呢?
1.如索引或者数据的扫描操作。
2.比如全表扫描,需要访问到表中的全部页,问题是缓冲池的大小是有限的,那么我这次全表扫描,如果全部放入到LRU列表的首部,那么非常可能会将比较重要的热点数据页从LRU列表中移出,当下一次需要读取热点数据的时候,又得去访问磁盘。
3.此外,InnoDB还引入一个时间参数innodb_old_blocks_time来保证:页读取到mid位置后需要等待多久会被加入到LRU列表的热端(new部分),以尽可能的让LRU列表中热点数据不被移出。
InnoDB引擎在1.0.x版本就开始支持页的压缩功能了。原本页的大小是16K,现在可以压缩为1、2、4、8KB,而这些非16KB的页,通过unzip_LRU列表来管理。
unzip_LRU列表对不同压缩页大小的页进行分别管理,例如需要从缓冲池中申请页为4KB的大小,过程如下:
- 检查4KB的unzip_LRU列表,检查是否有可用的空闲页。
- 如果有,直接使用。
- 如果没有,检查8KB的unzip_LRU列表。
- 如果能够得到空闲页,将页分成2个4KB页,存放到4KB的unzip_LRU列表。
- 如果不能得到空闲页,从LRU列表中申请一个16KB的页,将页分为1个8KB的页、2个4KB的页,分别存放到对应的unzip_LRU列表中。
在LRU列表中的页如果被修改了,那么这种页叫做脏页。 即缓冲池中的页he磁盘上的页的数据产生了不一致,而这个时候数据库会通过Checkpoint机制将脏页刷新到磁盘,而Flush列表中的页即为脏页列表。
注意:
- 脏页既存在于LRU列表,也存在与Flush列表中。
- LRU列表用来管理缓冲池中页的可用性,Flush列表用来管理将页刷新到磁盘,两者是独立的互相不影响的。
(3)重做日志缓冲
InnoDB存储引擎的内存区域除了有缓冲池外,还有重做日志缓冲(redo log buffer)。InnoDB存储引擎首先将重做日志信息放到该缓冲区中,然后按照一定的频率将其刷新到重做日志文件中,默认重做日志缓冲的大小为8MB,参数由innodb_log_buffer_size控制
缓冲刷新至文件的3种时机:
- Master Thread每秒将重做日志缓冲刷新到重做日志文件。
- 每个事务提交的时候会将重做日志缓冲刷新到重做日志文件。
- 当重做日志缓冲池剩余空间小于1/2时,重做日志缓冲刷新到重做日志文件。
(4)额外内存池
在InnoDB存储引擎中,堆内存的管理是通过一种称为内存堆的方式进行的。在对一些数据结构本身的内存进行分配的时候,需要从额外的内存池中进行申请,当该区域的内存不够的时候,才会从缓冲池进行申请。
Checkpoint
为了避免数据库发生数据丢失,当前事务数据库系统普遍采用了一种策略叫做:Write Ahead Log策略。 即事务提交的时候,先写重做日志,再修改页。而发生宕机而导致数据丢失的时候,就可以通过重做日志来完成数据的恢复。
Checkpoint技术的目的就是解决以下几个问题:
- 缩短数据库的恢复时间。
- 缓冲池不够用,将脏页刷新到磁盘。
- 重做日志不可用时,刷新脏页。
重做日志出现不可用的原因?
回答:
因为当前事务数据库系统对重做日志的设计都是循环使用的,并不是让日志无限增大,重做日志可以被重用的部分是指这些日志已经不再需要了,那么这部分就可以被覆盖。但是万一覆盖之后,之前的部分又需要使用了,那么必须强制产生Checkpoint,将缓冲池中的页至少刷新到当前重做日志的位置。
此外,当数据库发生宕机需要恢复的时候,不需要重做所有的日志,因为Checkpoint之前的页已经能保证刷新到磁盘中了,所以数据库只需要对之后的重做日志进行恢复即可,这样大大缩短了恢复的时间。并且,当缓冲池不够用的时候,根据LRU算法会把最近最少使用的页给移除,如果该页为脏页,那么需要强制执行Checkpoint,将脏页刷回磁盘。
InnoDB存储引擎内部有两种Checkpoint:
Sharp Checkpoint:发生在数据库关闭时,将所有的脏页刷新到磁盘,默认工作方式。 通过参数
innodb_fast_shutdown=1
实现Fuzzy Checkpoint:只刷新一部分脏页。 使用Fuzzy Checkpoint的情况:
Master Thread Checkpoint
以每秒或者每十秒的速度从缓冲池的脏页列表中刷新一定比例的页回到磁盘。
- FLUSH_LRU_LIST Checkpoint
因为InnoDB引擎需要保证LRU列表中需要有差不多100个空闲页可供使用,所以如果没有足够的空闲页,那么InnoDB引擎会将LRU列表尾端的页移除,那如果有脏页,则进行Checkpoint。 通过参数innodb_lru_scan_depth指定,默认1024
- Async/Sync Flush Checkpoint
指的是重做日志文件不可用的情况,这时候需要强制将一些页刷新到磁盘,为了保证重做日志的循环使用。
- Dirty Page too much Checkpoint
当脏页的数量太多,导致InnoDB存储引擎强制进行Checkpoint,目的是为了保证缓冲池中有足够的可用的页。通过参数innodb_max_dirty_pages_pct控制
默认大小75%,即缓冲池中脏页数量占据75%时候,强制进行Checkpoint,刷新一部分的脏页到磁盘。
InnoDB关键特性
插入缓冲
Insert Buffer
在InnoDB存储引擎中,主键是行唯一的标识符大家都知道,通常应用程序中行记录的插入顺序是按照主键递增的顺序进行插入的。 也因此,插入聚簇索引一般是顺序的,不需要磁盘的随机读取。
问题来了:一张表一般除了一个主键,还有多个非聚簇索引,我这里假设一张表中的聚簇索引为a,非聚簇索引为b,如:
CREATE TABLE t(
a int AUTO_INCREMENT,
b VARCHAR(30),
PRIMARY KEY(a),
KEY(b)
);
那么这张表在进行插入操作的时候,页的存放还是按照主键a进行顺序存放的,但是对于非聚簇索引b的叶子节点的插入不再是顺序的了,这时候需要离散的访问非聚簇索引页,即随机读取。(B+树的特性决定了非聚簇索引插入的离散型)。
那么,InnoDB存储引擎开创了Insert Buffer的目的是啥呢?
对于非聚簇索引的插入或者更新操作,不是每一次都直接插入到索引页中,而是先判断插入的非聚簇索引页是否在缓冲池中。
如果在——>直接插入。
如果不在——>先放入到Insert Buffer对象中,然后再以一定的频率将Insert Buffer和辅助索引页子节点进行合并操作。
那么这时候能够将多个插入合并到一个操作中,就大大提高了对于非聚簇索引插入的性能。
那么以我的理解:
- 把Insert Buffer看做是一个大容器,把每次非聚簇索引的插入当做一次任务。
- 因为非聚簇索引的插入具有离散型,那么如果把多个非聚簇索引的插入绑定在一块,形成一个大的插入事件,提高插入性能。
使用Insert Buffer需要满足的条件,有俩:
- 索引是辅助索引(非聚簇)。
- 索引不是唯一的(unique)。
Change Buffer
前面的Insert Buffer可以说是针对非聚簇索引的插入操作,那么自从InnoDB1.0.x版本后,引入了Insert Buffer的升级版:Change Buffer,可以对DML操作(insert,update,delete)都进行缓冲,这里分别对应了Insert Buffer、Purge Buffer、Delete Buffer,使用参数innodb_change_buffering
来开启各种Buffer选项。(默认是all,即全开启)
值得注意的是,因为Change Buffer是升级版,所以它适用的对象依然是非唯一的辅助索引,比如对一条记录进行update操作可以分为2个过程:
- 将记录标记为已删除。(对应Delete Buffer)
- 真正将记录删除。(对应Purge Buffer)
Insert Buffer的内部实现原理
Insert Buffer的数据结构是一颗B+树。 目前版本全局只有一颗Insert Buffer B+树,负责对所有的表的辅助索引进行Insert Buffer。这颗B+树存放在共享表空间中,默认是ibdata1中,其非叶子节点存放的是查询的search key(键值)
search key的结构:
- space(4字节):待插入记录所在表的表空间id(唯一),可以通过该id得知哪张表。
- marker(1字节):兼容老版本的Insert Buffer。
- offset(4字节):页所在的偏移量
插入原理:
当一个辅助索引需要插入到页中的时候,如果这个也不在缓冲池中,那么InnoDB存储引擎首先会根据上述的数据结构构造一个search key,接下来查询Insert Buffer这颗B+树,然后江浙条记录插入到其叶子节点中。
插入后的结构:
如图:Insert Buffer叶子节点中的记录,相比之前,多了一个metadata
其中metadata的存储内容包括(4字节):
其中IBUF_REC_OFFSET_COUNT用来排序每个记录进入Insert Buffer的顺序。 并且这里大家可以看出,Insert Buffer的B+树存储叶子节点,需要额外的13字节的开销(9字节的search key和4字节的metadata),后续的列表就是记录的实际字段了。
Merge Insert Buffer
插入缓冲的时候,如果插入记录的辅助索引页不在缓冲池中,记录将插入到Insert Buffer B+树中。那么什么时候把Insert Buffer的记录合并到真正的辅助索引中呢?
直接总的来说,Merge Insert Buffer的时机有这么3种:
- 辅助索引页被读取到缓冲池。
比如执行select操作,这时候需要先检查Insert Buffer Bitmap页,确认该辅助索引页是否有记录存放在Insert Buffer B+树中,如果有,则把树中的记录插入到辅助索引页中。
注意,注意:大家可以理解为,Insert Buffer B+树只是一个中间件,缓存记录的地方,而辅助索引页是辅助索引记录的最终归宿。
- Insert Buffer Bitmap页追踪到该辅助索引页已经没有可用空间的时候。
若插入辅助索引记录时检测到插入记录后辅助索引页的可用空间小于1/32,那么这个时候会强制进行Merge Insert Buffer。
- Master Thread。
每秒或者每十秒会进行一次Merge Insert Buffer操作。
上文多次提到了Insert Buffer Bitmap,这里来稍微解释下是个啥东西:
为了保证每次Merge Insert Buffer页必须成功,需要一个特殊的页来标记每个辅助索引页的可用空间,而这个也的类型为Insert Buffer Bitmap。
其结构:
二次写
Insert Buffer带给InnoDB存储引擎的是性能上的提升,而二次写(doublewrite)带给他的是数据页的可靠性。
再讲二次写之前,先给解释两个专有名词:写失效
当发生数据库宕机的时候,可能InnoDB存储引擎正在写入某个页到表中,而这个页中只写了一部分,比如一共16KB的页,只写了前4KB,之后发生了宕机,那么这种情况称之为写失效。
虽然发生写失效的时候,可以通过重做日志来进行恢复,但是重做日志中记录的是对页的物理操作,如果这个页本身发生了损坏,那么重做是没有意义的。 因此,我们需要在重做日志前,用户需要一个页的副本,当写入失效发生的时候,先通过页的副本来还原,再进行重做,这就是二次写。
doublewrite的结构如下,由两个部分组成:
- 一部分是内存中的doublewrite buffer,大小为2MB。
- 一部分是物理磁盘上共享表空间中连续的128个页,大小为2MB。
工作原理:
- 在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是通过memcpy函数将脏页先复制到内存当中的doublewrite buffer。
- 之后通过doublewrite buffer分两次操作,每次1MB,顺序的写入共享表空间的物理磁盘上。
- 调用fsync函数,同步磁盘,避免缓冲写带来的问题。
- 完成doublewrite页的写入后,再将doublewrite buffer中的页写入各个表空间文件中。
问题:如果操作系统在将页写入磁盘的过程中发生了崩溃,怎么办?
回答:
恢复过程中,InnoDB存储引擎可以从共享表空间中的doublewrite中找到该页的一个副本,将其复制到表空间文件中,再应用重做日志进行恢复。
自适应哈希索引
首先大家应该知道一点,哈希是一种非常快的查找方法,一般时间复杂度为O(1),B+树的查找次数,取决于B+树的高度。InnoDB会监控表上各个索引页的查询,如果说观察到建立哈希索引可以带来速度提升,那么会建立一个哈希索引,也就是自适应哈希索引(Adaptive Hash Index ,AHI)。
AHI是通过缓冲池的B+树页构造而来,因此建立的速度很快,InnoDB会自动根据访问的频率和模式来自动的为某一些热点页建立哈希索引,并且默认AHI功能默认开启。
异步IO和刷新邻接页
首先说下异步IO:
很简单,就是用户发起一个IO请求后立即在发送一个IO请求,当所有IO请求发送完毕后,等待所有IO操作的完成,核心是无需等待第一个IO请求的返回结果。
AIO的另一个优势就是可以进行IO Merge操作,也就是将多个IO操作合并为1个IO,这样可以提高IOPS(可以视为是每秒的读写次数)的性能。这里举一个书中的例子:
例如用户需要访问页的(space,page_no)为:(8,6),(8,7),(8,8)
那么每个页的大小为16KB,那么同步IO需要进行3次IO操作,而AIO会判断到这3个页是连续的,因此AIO底层会发送一个IO请求,从(8,6)开始,一次性读取48KB的页。
再来说下刷新邻接页:
InnoDB存储引擎提供了刷新邻接页(Flush Neighbor Page)的特性,其工作原理为:
- 当刷新一个脏页的时候,InnoDB存储引擎会检测到该页所在区的所有页。
- 如果是脏页,那么一个区的所有页一起进行刷新。
通过AIO可以将多个IO写入操作合并为一个IO操作,那么该工作机制在传统的机械硬盘下肯定是有显著的优势,但是也产生了俩问题:
- 如果将不怎么脏的页进行了写入,但是之后页又很快变成了脏页咋办?
- 固态硬盘有着较高的IOPS,是否还需要这个特性?