提起MySQL,其实网上已经有一大把教程了,为何我还要写这篇文章呢,大概是由于网上不少网站都是比较零散,并且描述不够直观,不能系统对MySQL相关知识有一个系统的学习,致使不能造成知识体系。为此我撰写了这篇文章,试图让这些底层架构相关知识更加直观易懂:php
图文
的方式描述技术原理;官网
或者技术书籍
来源,方便你们进一步扩展学习;背景知识
尽量作一个交代,好比讨论到log buffer的刷盘方式,延伸一下IO写磁盘相关知识点。好了,MySQL从不会到精通系列立刻就要开始了(看完以后仍是不会的话..请忽略这句话)。html
可能会有同窗问:为啥不直接学更加先进的TiDB,或者是强大的OceanBase。mysql
其实,MySQL做为老牌的应用场景普遍的关系型开源数据库,其底层架构是很值得咱们学习的,吸取其设计精华,那么咱们在平时的方案设计工做中也能够借鉴,若是项目中用的是MySQL,那么就可以把数据库用的更好了,了解了MySQL底层的执行原理,对于调优工做也是有莫大帮助的。本文我重点讲述MySQL底层架构,涉及到:linux
buffer pool
、log buffer
、change buffer
,buffer pool的页淘汰机制是怎样的;系统表空间
、独立表空间
、通用表空间
、undo表空间
、redo log
;IO
相关底层原理、查询SQL执行流程
、数据页结构
和行结构
描述、汇集索引
和辅助索引
的底层数据组织方式、MVCC
多版本并发控制的底层实现原理,以及可重复读
、读已提交
是怎么经过MVCC实现的。看完文本文,您将了解到:算法
IO操做
写磁盘有哪几种方式,有什么IO优化方式 (3.1.二、关于磁盘IO的方式)InnoDB缓存
(buffer pool, log buffer)的刷新方式有哪些(3.1.2.二、innodb_flush_method)log buffer
的日志刷盘控制参数innodb_flush_log_at_trx_commit
对写性能有什么影响(3.4.一、配置参数)表空间
(系统表空间,独立表空间,通用表空间)的做用和优缺点是什么,ibdata
、ibd
、frm
文件分别是干吗的(3.五、表空间)varchar
,null
底层是如何存储的,最大可用存储多大的长度(3.6.3.一、MySQL中varchar最大长度是多少)索引
的组织方式是怎样的,明白为何要采用B+树
,而不是哈希表、二叉树或者B树(3.七、索引 - 为何MySQL使用B+树)大字段
会影响表性能(查询性能,更新性能)(3.七、索引)覆盖索引
、联合索引
什么状况下会生效(3.7.二、辅助索引)索引下推
,索引下推减小了哪方面的开销?(3.7.二、辅助索引 - 索引条件下推)Change Buffer
对二级索引DML语句有什么优化(3.二、Change Buffer)redo log
、undo log
和buffer pool
数据完整性的关键做用分别是什么(3.10.二、如何保证数据不丢失)MVCC
底层是怎么实现的,可重复读和读已提交是怎么实现的(3.11.二、MVCC实现原理)以下图为MySQL架构涉及到的经常使用组件:sql
有以下表格:数据库
咱们执行如下sql:编程
select * from t_user where user_id=10000;
以下图,创建过程:后端
对于Java应用程序来讲,通常会把创建好的链接放入数据库链接池中进行复用,只要这个链接不关闭,就会一直在MySQL服务端保持着,能够经过show processlist
命令查看,以下:数组
注意,这里有个Time,表示这个链接多久没有动静了,上面例子是656秒没有动静,默认地,若是超过8个小时尚未动静,链接器就会自动断开链接,能够经过wait_timeout
参数进行控制。
以下图,执行sql:
以下图,为存储引擎的架构:
其实内存中的结构不太好直接观察到,不过磁盘的仍是能够看到的,咱们找到磁盘中MySQL的数据文件夹看看:
cd innodb_data_home_dir
查看MySQL 数据目录:
|- ib_buffer_pool // 保存缓冲池中页面的表空间ID和页面ID,用于重启恢复缓冲池 |- ib_logfile0 // redo log 磁盘文件1 |- ib_logfile1 // redo log 磁盘文件2,默认状况下,重作日志存在磁盘的这两个文件中,循环的方式写入重作日志 |- ibdata1 // 系统表空间文件 |- ibtmp1 // 默认临时表空间文件,可经过innodb_temp_data_file_path属性指定文件位置 |- mysql/ |- mysql-bin.000001 // bin log文件 |- mysql-bin.000001 // bin log文件 ... |- mysql-bin.index // bin log文件索引 |- mysqld.local.err // 错误日志 |- mysqld.local.pid // mysql进程号 |- performance_schema/ // performance_schema数据库 |- sys/ // sys数据库 |- test/ // 数据库文件夹 |- db.opt // test数据库配置文件,包含数据库字符集属性 |- t.frm // 数据表元数据文件,无论是使用独立表空间仍是系统表空间,每一个表都对应有一个 |- t.ibd // 数据库表独立表空间文件,若是使用的是独立表空间,则一个表对应一个ibd文件,不然保存在系统表空间文件中
接下来咱们逐一来介绍。
buffer pool
(缓冲池
)是主内存
中的一个区域,在InnoDB访问表数据
和索引数据
的时候,会顺便把对应的数据页缓存到缓冲池中。若是直接从缓冲池中直接读取数据将会加快处理速度。在专用服务器上,一般将80%左右的物理内存分配给缓冲池。
为了提升缓存管理效率,缓冲池把页面连接为列表,使用改进版的LRU算法
将不多使用的数据从缓存中老化淘汰掉。
经过使用改进版的LRU算法来管理缓冲池列表。
当须要把新页面存储到缓冲池中的时候,将淘汰最近最少使用的页面,并将新页面添加到旧子列表的头部。
该算法运行方式:
相关优化参数:
innodb_old_blocks_pct
:控制LRU列表中旧子列表的百分比,默认是37,也就是3/8,可选范围为5~95;innodb_old_blocks_time
:指定第一次访问页面后的时间窗口,该时间窗口内访问页面不会使其移动到LRU列表的最前面。默认是1000,也就是1秒。innodb_old_blocks_time很重要,有了这1秒,对于全表扫描,因为是顺序扫描的,通常同一个数据页的数据都是在一秒内访问完成的,不会升级到新子列表中,一直在旧子列表淘汰数据,因此不会影响到新子列表的缓存。
O_DIRECT
是innodb_flush_method
参数的一个可选值。
这里先介绍下和数据库性能密切相关的文件IO操做方法
数据库系统是基于文件系统的,其性能和设备读写的机制有密切的关系。
int open(const char *pathname, int flags);
系统调用Open会为该进程一个文件描述符fd,经常使用的flags以下:
O_WRONLY
:表示咱们以"写"的方式打开,告诉内核咱们须要向文件中写入数据;O_DSYNC
:每次write都等待物理I/O完成,可是若是写操做不影响读取刚写入的数据,则不等待文件属性更新;O_SYNC
:每次write都等到物理I/O完成,包括write引发的文件属性的更新;O_DIRECT
:执行磁盘IO时绕过缓冲区高速缓存(内核缓冲区),从用户空间直接将数据传递到文件或磁盘设备,称为直接IO(direct IO)。由于没有了OS cache,因此会O_DIRECT下降文件的顺序读写的效率。ssize_t write(int fd, const void *buf, size_t count);
使用open打开文件获取到文件描述符以后,能够调用write函数来写文件,具体表现根据open函数参数的不一样而不一样弄。
#include <unistd.h> int fsync(int fd); int fdatasync(int fd);
fdatasync
:操做完write以后,咱们能够调用fdatasync将文件数据块flush到磁盘,只要fdatasync返回成功,则能够认为数据已经写到磁盘了;fsync
:与O_SYNC参数相似,fsync还会更新文件metadata到磁盘;sync
:sync只是将修改过的块缓冲区写入队列,而后就返回,不等实际写磁盘操做完成;为了保证文件更新成功持久化到硬盘,除了调用write方法,还须要调用fsync。
大体交互流程以下图:
更多关于磁盘IO的相关内容,能够阅读:On Disk IO, Part 1: Flavors of IO[9]
fsync性能问题:除了刷脏页到磁盘,fsync还会同步文件metadata,而文件数据和metadata一般存放在磁盘不一样地方,因此fsync至少须要两次IO操做。
对fsync性能的优化建议:因为以上性能问题,若是可以减小metadata的更新,那么就可使用fdatasync了。所以须要确保文件的尺寸在write先后没有发生变化。为此,能够建立固定大小的文件进行写,写完则开启新的文件继续写。
innodb_flush_method
定义用于将数据刷新到InnoDB
数据文件和日志文件的方法,这可能会影响I/O吞吐量。
如下是具体参数说明:
属性 | 值 |
---|---|
命令行格式 | --innodb-flush-method=value |
系统变量 | innodb_flush_method |
范围 | 全局 |
默认值(Windows) | unbuffered |
默认值(Unix) | fsync |
有效值(Windows) | unbuffered, normal |
有效值(Unix) | fsync, O_DSYNC, littlesync, nosync, O_DIRECT, O_DIRECT_NO_FSYNC |
比较经常使用的是这三种:
默认值,使用fsync()
系统调用来flush数据文件和日志文件到磁盘;
因为open函数的O_DSYNC参数在许多Unix系统上都存中问题,所以InnoDB不直接使用O_DSYNC。
InnoDB
用于O_SYNC
打开和刷新日志文件,fsync()
刷新数据文件。
表现为:写日志操做是在write函数完成,数据文件写入是经过fsync()
系统调用来完成;
使用O_DIRECT
(在Solaris上对应为directio()
)打开数据文件,并用于fsync()
刷新数据文件和日志文件。此选项在某些GNU/Linux版本,FreeBSD和Solaris上可用。
表现为:数据文件写入直接从buffer pool到磁盘,不通过操做系统缓冲,日志仍是须要通过操做系统缓存;
在刷新I/O期间InnoDB
使用O_DIRECT
,而且每次write操做后跳过fsync()
系统调用。
此设置适用于某些类型的文件系统,但不适用于其余类型的文件系统。例如,它不适用于XFS。若是不肯定所使用的文件系统是否须要fsync()(例如保留全部文件元数据),请改用O_DIRECT。
以下图所示:
为何使用了O_DIRECT配置后还须要调用fsync()?
参考MySQL的这个bug:Innodb calls fsync for writes with innodb_flush_method=O_DIRECT[10]
Domas进行的一些测试代表,若是没有fsync,某些文件系统(XFS)不会同步元数据。若是元数据会更改,那么您仍然须要使用fsync(或O_SYNC来打开文件)。
例如,若是在启用O_DIRECT的状况下增大文件大小,它仍将写入文件的新部分,可是因为元数据不能反映文件的新大小,所以若是此刻系统发生崩溃,文件尾部可能会丢失。
为此:当重要的元数据发生更改时,请继续使用fsync或除O_DIRECT以外,也能够选择使用O_SYNC。
MySQL从v5.6.7起提供了O_DIRECT_NO_FSYNC
选项来解决此类问题。
change buffer是一种特殊的数据结构,当二级索引页(非惟一索引)不在缓冲池中时,它们会缓存这些更改 。当页面经过其余读取操做加载到缓冲池中时,再将由INSERT
,UPDATE
或DELETE
操做(DML)产生的change buffer合并到buffer pool的数据页中。
为何惟一索引不可使用chage buffer?
针对惟一索引,若是buffer pool不存在对应的数据页,仍是须要先去磁盘加载数据页,才能判断记录是否重复,这一步避免不了。
而普通索引是非惟一的,插入的时候以相对随机的顺序发生,删除和更新也会影响索引树中不相邻的二级索引树,经过使用合并缓冲,避免了在磁盘产生大量的随机IO访问获取普通索引页。
问题
当有许多受影响的行和许多辅助索引要更新时,change buffer合并可能须要几个小时,在此期间,I/O会增长,可能会致使查询效率大大下降,即便在事务提交以后,或者服务器重启以后,change buffer合并操做也会继续发生。相关阅读:Section 14.22.2, “Forcing InnoDB Recovery”
自适应哈希索引功能由innodb_adaptive_hash_index
变量启用 ,或在服务器启动时由--skip-innodb-adaptive-hash-index
禁用。
log buffer(日志缓冲区)用于保存要写入磁盘上的log file(日志文件)的数据。日志缓存区的内容会按期刷新到磁盘。
日志缓冲区大小由innodb_log_buffer_size
变量定义 。默认大小为16MB。较大的日志缓冲区可让大型事务在提交以前无需将redo log写入磁盘。
若是您有更新,插入或者删除多行的事务,尝试增大日志缓冲区的大小能够节省磁盘I/O。
innodb_flush_log_at_trx_commit
innodb_flush_log_at_trx_commit
变量控制如何将日志缓冲区的内容写入并刷新到磁盘。
该参数控制是否严格存储ACID仍是尝试获取更高的性能,能够经过该参数获取更好的性能,可是会致使在系统崩溃的过程当中致使数据丢失。
可选参数:
innodb_flush_log_at_timeout
innodb_flush_log_at_timeout
变量控制日志刷新频率。可以让您将日志刷新频率设置为N
秒(其中N
为1 ... 2700
,默认值为1)
为了保证数据不丢失,请执行如下操做:
- 若是启用了binlog,则设置:sync_binlog=1;
- innodb_flush_log_at_trx_commit=1;
配置效果以下图所示:
一个InnoDB
表及其索引能够在建在系统表空间中,或者是在一个 独立表空间 中,或在 通用表空间。
innodb_file_per_table
启用时,一般是将表存放在独立表空间中,这是默认配置;innodb_file_per_table
禁用时,则会在系统表空间中建立表;CREATE TABLE ... TABLESPACE
语法。有关更多信息,请参见官方文档 14.6.3.3 General Tablespaces。表空间概览图:
相关文件默认在磁盘中的innodb_data_home_dir
目录下:
|- ibdata1 // 系统表空间文件 |- ibtmp1 // 默认临时表空间文件,可经过innodb_temp_data_file_path属性指定文件位置 |- test/ // 数据库文件夹 |- db.opt // test数据库配置文件,包含数据库字符集属性 |- t.frm // 数据表元数据文件,无论是使用独立表空间仍是系统表空间,每一个表都对应有一个 |- t.ibd // 数据库表独立表空间文件,若是使用的是独立表空间,则一个表对应一个ibd文件,不然保存在系统表空间文件中
frm文件
建立一个InnoDB
表时,MySQL 在数据库目录中建立一个.frm文件。frm文件包含MySQL表的元数据(如表定义)。每一个InnoDB表都有一个.frm文件。
与其余MySQL存储引擎不一样, InnoDB
它还在系统表空间
内的自身内部数据字典中编码有关表的信息。MySQL删除表或数据库时,将删除一个或多个.frm
文件以及InnoDB
数据字典中的相应条目。
所以,在InnoDB中,您不能仅经过移动.frm
文件来移动表。有关移动InnoDB
表的信息,请参见官方文档14.6.1.4 Moving or Copying InnoDB Tables。
ibd文件
对于在独立表空间建立的表,还会在数据库目录中生成一个 .ibd表空间文件。
在通用表空间
中建立的表在现有的常规表空间 .ibd文件中建立。常规表空间文件能够在MySQL数据目录内部或外部建立。有关更多信息,请参见官方文档14.6.3.3 General Tablespaces。
ibdata文件
系统表空间文件,在 InnoDB
系统表空间中建立的表在ibdata中建立。
系统表空间由一个或多个数据文件(ibdata文件)组成。其中包含与InnoDB
相关对象有关的元数据(InnoDB
数据字典 data dictionary),以及更改缓冲区(change buffer), 双写缓冲区(doublewrite buffer)和撤消日志(undo logs)的存储区 。
InnoDB
若是表是在系统表空间中建立的,则系统表空间中也包含表的表数据和索引数据。
在MySQL 5.6.7以前,默认设置是将全部InnoDB
表和索引保留 在系统表空间内,这一般会致使该文件变得很是大。由于系统表空间永远不会缩小,因此若是先加载而后删除大量临时数据,则可能会出现存储问题。
在MySQL 5.7中,默认设置为 独立表空间模式,其中每一个表及其相关索引存储在单独的 .ibd文件中。此默认设置使使用Barracuda文件格式的InnoDB
功能更容易使用,例如表压缩,页外列的有效存储以及大索引键前缀(innodb_large_prefix
)。
将全部表数据保留在系统表空间或单独的 .ibd
文件中一般会对存储管理产生影响。
InnoDB
在MySQL 5.7.6中引入了通用表空间[11],这些表空间也由.ibd
文件表示 。通用表空间是使用CREATE TABLESPACE
语法建立的共享表空间。它们能够在MySQL数据目录以外建立,可以容纳多个表,并支持全部行格式的表。
MySQL 5.7中,配置参数:innodb_file_per_table
,默认处于启用状态,这是一个重要的配置选项,会影响InnoDB
文件存储,功能的可用性和I/O特性等。
启用以后,每一个表的数据和索引是存放在单独的.ibd文件中的,而不是在系统表空间的共享ibdata文件中。
数据压缩
[12]的行格式,如:
前缀索引
[13]最多包含768个字节。若是开启innodb_large_prefix,且Innodb表的存储行格式为 DYNAMIC 或 COMPRESSED,则前缀索引最多可包含3072个字节,前缀索引也一样适用;TRUNCATE TABLE
执行的更快,而且回收的空间不会继续保留,而是让操做系统使用;即便启用了innodb_file_per_table参数,每张表空间存放的只是数据、索引和插入缓存Bitmap页,其余数据如回滚信息、插入缓冲索引页、系统事务信息、二次写缓冲等仍是存放在原来的共享表空间中。
通用表空间使用CREATE TABLESPACE
语法建立。
相似于系统表空间,通用表空间是共享表空间,能够存储多个表的数据。
通用表空间比独立表空间具备潜在的内存优点,服务器在表空间的生存期内将表空间元数据保留在内存中。一个通用表空间一般能够存放多个表数据,消耗更少的表空间元数据内存。
数据文件能够放置在MySQL数据目录或独立于MySQL数据目录。
undo表空间包含undo log。
innodb_rollback_segments
变量定义分配给每一个撤消表空间的回滚段的数量。
undo log能够存储在一个或多个undo表空间中,而不是系统表空间中。
在默认配置中,撤消日志位于系统表空间中。SSD存储更适合undo log的I/O模式,为此,能够把undo log存放在有别于系统表空间的ssd硬盘中。
innodb_undo_tablespaces
配置选项控制undo表空间的数量。
由用户建立的非压缩临时表和磁盘内部临时表是在共享临时表空间中建立的。
innodb_temp_data_file_path
配置选项指定零时表空间文件的路径,若是未指定,则默认在 innodb_data_home_dir
目录中建立一个略大于12MB 的自动扩展数据文件ibtmp1
。
使用ROW_FORMAT=COMPRESSED
属性建立的压缩临时表,是在独立表空间中的临时文件目录中建立的 。
服务启动的时候建立临时表空间,关闭的时候销毁临时表空间。若是临时表空间建立失败,则意味着服务启动失败。
在介绍索引以前,咱们有必要了解一下InnoDB底层的逻辑存储结构,由于索引是基于这个底层逻辑存储结构建立的。截止到目前,咱们所展现的都仅仅是物理磁盘中的逻辑视图,接下来咱们就来看看底层的视图。
如今咱们打开一个表空间ibd文件,看看里面都是如何组织数据的?
以下图,表空间由段(segment)、区(extent)、页(page)组成。
InnoDB最小的存储单位是页,默认每一个页大小是16k。
而InnoDB存储引擎是面向行的(row-oriented),数据按行进行存放,每一个页规定最多容许存放的行数=16k/2 - 200,即7992行。
段:如数据段、索引段、回滚段等。InnoDB存储引擎是B+树索引组织的,因此数据即索引,索引即数据。B+树的叶子节点存储的都是数据段的数据。
名称 | 占用空间 | 描述 |
---|---|---|
Fil Header | 38 byte | 页的基本信息,如所属表空间,上一页和下一页指针。 |
Page Header | 56 byte | 数据页专有的相关信息 |
Infimun + Supremum | 26 byte | 两个虚拟的行记录,用于限定记录的边界 |
User Records | 动态分配 | 实际存储的行记录内容 |
Free Space | 动态调整 | 还没有使用的页空间 |
Page Directory | 动态调整 | 页中某些记录的相对位置 |
Fil Trailer | 8 byte | 校验页是否完整 |
关于Infimun和Supremum:首次建立索引时,InnoDB会在根页面中自动设置一个最小记录和一个最高记录,而且永远不会删除它们。最低记录和最高记录能够视为索引页开销的一部分。最初,它们都存在于根页面上,可是随着索引的增加,最低记录将存在于第一或最低叶子页上,最高记录将出如今最后或最大关键字页上。
先来说讲Compact行记录格式,Compact是MySQL5.0引入的,设计目标是高效的存储数据,让一个页可以存放更多的数据,从而实现更快的B+树查找。
名称 | 描述 |
---|---|
变长字段长度列表 | 字段大小最多用2个字节表示,也就是最多限制长度:2^16=65535个字节;字段大小小于255字节,则用1个字节表示; |
NULL标志位 | 记录该行哪些位置的字段是null值 |
记录头信息 | 记录头信息信息,固定占用5个字节 |
列1数据 | 实际的列数据,NULL不占用该部分的空间 |
列2数据 | |
... |
记录头用于将连续的记录连接在一块儿,并用于行级锁定。
每行数据除了用户定义的列外,还有两个隐藏列:
而记录头信息包[16]含以下内容:
名称 | 大小(bit) | 描述 |
---|---|---|
() | 1 | 未知 |
() | 1 | 未知 |
deleted_flag | 1 | 该行是否已被删除 |
min_rec_flag | 1 | 若是该记录是预约义的最小记录,则为1 |
n_owned | 4 | 该记录拥有的记录数 |
heap_no | 13 | 索引堆中该条记录的排序号 |
record_type | 3 | 记录类型:000 普通,001 B+树节点指针,010 Infimum,011 Supremum,1xx 保留 |
next_record | 16 | 指向页中下一条记录 |
更详细的页结构参考官网:22.2 InnoDB Page Structure
更详细的行结构参考官网:22.1 InnoDB Record Structure
更详细的行格式参考官网:14.11 InnoDB Row Formats
根据以上格式,能够得出数据页内的记录组织方式:
上面表格描述咱们知道,一个字段最长限制是65535个字节,这是存储长度的限制。
而MySQL中对存储是有限制的,具体参考:8.4.7 Limits on Table Column Count and Row Size
而实际可以存储的字符是跟编码有关的。
背景知识:
MySQL 4.0版本如下,varchar(10),表明10个字节,若是存放UTF8汉字,那么只能存3个(每一个汉字3字节);
MySQL 5.0版本以上,varchar(10),指的是10个字符,不管存放的是数字、字母仍是UTF8汉字(每一个汉字3字节),均可以存放10个,最大大小是65532字节;
所以,Mysql5根据编码不一样,存储大小也不一样。
那么假设咱们使用的是utf8编码,那么每一个字符最多占用3个字节,也就是最多定义varchar(21845)个字符,若是是ascii编码,一个字符至关于一个字节,最多定义varchar(65535)个字符,下面咱们验证下。
咱们尝试建立一个这样的字段:
CREATE TABLE `t10` ( `id` int(11) NOT NULL, `a` int(11) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB CHARSET=ascii ROW_FORMAT=Compact; alter table t10 add `str` varchar(21845) DEFAULT NULL; alter table t10 add `str` varchar(65535) DEFAULT NULL;
发现提示这个错误:
mysql> alter table t10 add `str` varchar(65535) DEFAULT NULL; ERROR 1118 (42000): Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs
缘由是按照以上的行格式介绍,变长字段长度列表
记录也须要占用空间,占用2个字节,另外这里是容许为空字段,在8位以内,因此NULL标志位占用1个字节,因此咱们总共能够存储的字符数是:
65535 - 2 - 2 - 4 - 4=65534
其中 -2 个字节表示变长字段列表,-1表示NULL标志位,两个-4表示两个int类型字段占用大小
因此实际上可以容纳的varchar大小为:65524,咱们验证下:
MySQL表的内部表示具备65,535字节的最大行大小限制。InnoDB
对于4KB,8KB,16KB和32KB innodb_page_size
设置,表的最大行大小(适用于本地存储在数据库页面内的数据)略小于页面的一半 。若是包含 可变长度列的InnoDB
行超过最大行大小,那么将选择可变长度列用于外部页外存储。
可变长度列因为太长而没法容纳在B树页面上,这个时候会把可变长度列存储在单独分配的磁盘页面上,这些页面称为溢出页面
,这些列称为页外列
。页外列的值存储在由溢出页面构成的单连接列表
中。
InnoDB
存储引擎支持四种行格式:REDUNDANT
,COMPACT
, DYNAMIC
,和COMPRESSED
。不一样的行格式,对溢出的阈值和处理方式有所区别,详细参考:14.11 InnoDB Row Formats。
COMPACT行格式处理方式
使用COMPACT
行格式的表将前768个字节的变长列值(VARCHAR
, VARBINARY
和 BLOB
和 TEXT
类型)存储在B树节点内的索引记录中,其他的存储在溢出页上。
若是列的值等于或小于768个字节,则不使用溢出页,所以能够节省一些I / O。
若是查过了768个字节,那么会按照以下方式进行存储:
DYNAMIC行格式处理方式
DYNAMIC
行格式提供与COMPACT
行格式相同的存储特性,但改进了超长可变长度列的存储能力和支持大索引键前缀。
InnoDB
能够彻底在页外存储过长的可变长度列值(针对 VARCHAR
, VARBINARY
和 BLOB
和 TEXT
类型),而汇集索引记录仅包含指向溢出页的20字节指针。大于或等于768字节的固定长度字段被编码为可变长度字段。
表中大字段引起的问题
若是一个表中有过多的可变长度大字段,致使一行记录太长,而整个时候使用的是COMPACT行格式,那么就可能会插入数据报错。
如,页面大小事16k,根据前面描述咱们知道,MySQL限制一页最少要存储两行数据,若是不少可变长度大字段,在使用COMPACT的状况下,仍然会把大字段的前面768个字节存在索引页中,能够算出最多支持的大字段:
1024 * 16 / 2 / 768 = 10.67
,那么超过10个可变长度大字段就会插入失败了。这个时候能够把row format改成:DYNAMIC。
前面咱们了解了InnoDB底层的存储结构,即:以B+树的方式组织数据页。另外了解了数据页中的数据行的存储方式。
而构建B+树索引的时候必需要选定一个或者多个字段做为索引的值,若是索引选择的是主键,那么咱们就称为汇集索引,不然就是二级索引。
为何MySQL使用B+树?
- 哈希表虽然能够提供O(1)的单行数据操做性能,但却不能很好的支持排序和范围查找,会致使全表扫描;
- B树能够再非叶子节点存储数据,可是这可能会致使查询连续数据的时候增长更多的I/O操做;
- 而B+树数据都存放在叶子节点,叶子节点经过指针相互链接,能够减小顺序遍历时产生的额外随机I/O
更新详细解释: 为何 MySQL 使用 B+ 树[17]
了解到上面的底层逻辑存储结构以后,咱们进一步来看看InnoDB是怎么经过B+树来组织存储数据的。
首先来介绍下汇集索引。
主键索引的InnoDB术语。
下面咱们建立一张测试表,并插入数据,来构造一颗B+树:
CREATE TABLE t20 ( id int NOT NULL, a int NOT NULL, b int, c int, PRIMARY KEY (`id`) ) ENGINE=InnoDB; insert into t20 values(20, 1, 2, 1); insert into t20 values(40, 1, 2, 5); insert into t20 values(30, 3, 2, 4); insert into t20 values(50, 3, 6, 2); insert into t20 values(10, 1, 1, 1);
能够看到,虽然咱们是id乱序插入的,可是插入以后查出来的确是排序好的:
这个排序就是B+索引树构建的。
咱们能够经过这个在线的动态演示工具来看看B+树的构造过程,最终结果以下:
实际存放在数据库中的模型因页面大小不同而有所不一样,这里为了简化模型,咱们按照B+树的通用模型来解释数据的存储结构。
相似的,咱们的数据也是这种组织形式的,该B+树中,咱们以主键为索引进行构建,而且把完整的记录存到对应的页下面:
其中蓝色的是索引页,橙色的是数据页。
每一个页的大小默认为16k,若是插入新的数据行,这个时候就要申请新的数据页了,而后挪动部分数据过去,从新调整B+树,这个过程称为页分裂
,这个过程会影响性能。
相反的,若是InnoDB
索引页的填充因子降低到之下MERGE_THRESHOLD
,默认状况下为50%(若是未指定),则InnoDB
尝试收缩索引树以释放页面。
自增主键的插入是递增顺序插入的,每次添加记录都是追加的,不涉及到记录的挪动,不会触发叶子节点的分裂,而通常业务字段作主键,每每都不是有序插入的,写成本比较高,因此咱们更倾向于使用自增字段做为主键。
PRIMARY KEY
以后,InnoDB会把它做为汇集索引。为此,为你的每一个表定义一个PRIMARY KEY
。若是没有惟一而且非空的字段或者一组列,那么请添加一个自增列;PRIMARY KEY
,则MySQL会找到第一个不带null值的UNIQUE索引,并其用做汇集索引;PRIMARY KEY
或没有合适的UNIQUE
索引,则InnoDB
内部会生成一个隐藏的汇集索引GEN_CLUST_INDEX
,做为行ID,行ID是一个6字节的字段,随着数据的插入而自增。根据索引进行查找id=50的记录,以下图,沿着B+树一直往下寻找,最终找到第四页,而后把该页加载到buffer pool中,在缓存中遍历对比查找,因为里面的行记录是顺序组织的,因此很快就能够定位到记录了。
除了汇集索引以外的全部索引都称为辅助索引(二级索引)。在InnoDB中,辅助索引中每一个记录都包含该行的主键列以及为辅助索引指定的列。
在辅助索引中查找到记录,能够获得记录的主键索引ID,而后能够经过这个主键索引ID去汇集索引中搜索具体的记录,这个过程称为回表操做。
若是主键较长,则辅助索引将使用更多空间,所以具备短的主键是有利的。
下面咱们给刚刚的表添加一个组合联合索引
-- 添加多一个字段 alter table t20 add column d varchar(20) not null default ''; -- 添加一个联合索引 alter table t20 add index idx_abc(a, b, c);
添加以后组合索引B+树以下,其中索引key为abc三个字段的组合,索引存储的记录为主键ID:
InnoDB存储引擎支持覆盖索引,即从辅助索引中就能够获得查询的记录,而不须要回表去查询汇集索引中的记录,从而减小大量的IO操做。下面的查询既是用到了覆盖索引 idx_abc:
select a, b from t20 where a > 2;
执行结果以下:
能够发现,Extra这一列提示Using index,使用到了覆盖索引,扫描的行数为2。注意:这里的扫描行数指的是MySQL执行器从引擎取到两条记录,引擎内部可能会遍历到多条记录进行条件比较。
因为InnoDB索引式B+树构建的,所以能够利用索引的“最左前缀”来定位记录。
也就是说,不只仅是用到索引的所有定义字段会走索引,只要知足最左前缀,就能够利用索引来加速检索。这个最左前缀能够是联合索引的最左n个字段。
索引条件下推 Index Condition Pushdown (ICP),是针对MySQL使用索引从表中检索行的状况的一种优化。
为何叫下推呢,就是在知足要求的状况下,把索引的条件丢给存储引擎去判断,而不是把完整的记录传回MySQL Server层去判断。
ICP支持range
, ref
, eq_ref
, 和 ref_or_null
类型的查找,支持MyISAM和InnoDB存储引擎。
不能将引用子查询的条件下推,触发条件不能下推。详细规则参考:Index Condition Pushdown
若是不使用ICP,则存储引擎将遍历索引以在汇集索引中定位行,并将结果返回给MySQL Server层,MySQL Server层继续根据WHERE
条件进行筛选行。
启用ICP后,若是WHERE
能够仅使用索引中的列来评估部分条件,则MySQL Server层会将这部分条件压入WHERE
条件降低到存储引擎。而后,存储引擎经过使用索引条目来判断索引条件,在知足条件的状况下,才回表去查找记录返回给MySQL Server层。
ICP的目标是减小回表扫描的行数,从而减小I / O操做。对于InnoDB
表,ICP仅用于二级索引。
使用索引下推的时候,执行计划中的Extra会提示:Using index condition
,而不是Using index
,由于必须回表查询整行数据。Using index
表明使用到了覆盖索引。
InnoDB数据字典(Data Directory)存放于系统表空间中,主要包含元数据,用于追踪表、索引、表字段等信息。因为历史的缘由,InnoDB数据字典中的元数据与.frm
文件中的元数据重复了。
双写缓冲区(Doublewrite Buffer)是一个存储区,是InnoDB在tablespace上的128个页(2个区),大小是2MB[18]。
版本区别:在MySQL 8.0.20以前,doublewrite缓冲区存储区位于
InnoDB
系统表空间中。从MySQL 8.0.20开始,doublewrite缓冲区存储区位于doublewrite文件中。本文基于MySQL 5.7编写。
操做系统写文件是以4KB为单位的,那么每写一个InnoDB的page到磁盘上,操做系统须要写4个块。若是写入4个块的过程当中出现系统崩溃,那么会致使16K的数据只有一部分写是成功的,这种状况下就是partial page write
(部分页写入)问题。
InnoDB这个时候是无法经过redo log来恢复的,由于这个时候页面的Fil Trailer
(Fil Trailer 主要存放FIL_PAGE_END_LSN
,主要包含页面校验和以及最后的事务)中的数据是有问题的。
为此,每当InnoDB将页面写入到数据文件中的适当位置以前,都会首先将其写入双写缓冲区。只有将缓冲区安全地刷新到磁盘后,InnoDB才会将页面写入最终的数据文件。
若是在页面写入过程当中发生操做系统或者mysqld进程崩溃,则InnoDB能够在崩溃恢复期间从双写缓冲区中找到页面的无缺副本用于恢复。恢复时,InnoDB扫描双写缓冲区,并为缓冲区中的每一个有效页面检查数据文件中的页面是否完整。
若是系统表空间文件(“ ibdata文件 ”)位于支持原子写的Fusion-io设备上,则自动禁用双写缓冲,而且将Fusion-io原子写用于全部数据文件。
重作日志(Redo Log)主要适用于数据库的崩溃恢复,用于实现数据的完整性。
重作日志由两部分组成:
ib_logfile0
和ib_logfile1
的物理文件表示。为了实现数据完整性,在脏页刷新到磁盘以前,必须先把重作日志写入到磁盘。除了数据页,汇集索引、辅助索引以及Undo Log都须要记录重作日志。
在事务中,除了写Redo log,还须要写binlog,为此,咱们先来简单介绍下binlog。
全写:Binary Log,二进制log。二进制日志是一组日志文件。其中包含有关对MySQL服务器实例进行的数据修改的信息。
Redo Log是InnoDB引擎特有的,而binlog是MySQL的Server层实现的,全部引擎均可以使用。
Redo Log的文件是循环写的,空间会用完,binlog日志是追加写的,不会覆盖之前的日志。
binlog主要的目的:
binlog主要是用于主从同步和数据恢复,Redo Log主要是用于实现事务数据的完整性,让InnoDB具备不会丢失数据的能力,又称为crash-safe。
binlog日志的两种记录形式:
混合日志记录默认状况下使用基于语句的日志记录,但根据须要自动切换到基于行的日志记录。
简单的介绍完binlog,咱们再来看看Redo Log的写入流程。
假设咱们这里执行一条sql
update t20 set a=10 where id=1;
执行流程以下:
前面咱们介绍Log Buffer
的时候,提到过,为了保证数据不丢失,咱们须要执行如下操做:
- sync_binlog=0:表示每次提交事务都只 write,不 fsync;
- sync_binlog=1:表示每次提交事务都会执行 fsync;
- sync_binlog=N(N>1) :表示每次提交事务都 write,但累积 N 个事务后才 fsync。
这两个的做用至关于在上面的流程最后一步,提交事务接口返回Server层以前,把binlog cache和log buffer都fsync到磁盘中了,这样就保证了数据的落盘,不会丢失,即便奔溃了,也能够经过binlog和redo log恢复数据相关流程以下:
在磁盘和内存中的处理流程以下面编号所示:
其中第四步log buffer持久化到磁盘的时机为:
innodb_log_buffer_size
一半的时候,后台线程主动写盘;其中第五步:脏页刷新到磁盘的时机为:
参数
innodb_max_dirty_pages_pct
是脏页比例上限,默认值是 75%。
为何第二步 redo log prepare状态也要写磁盘?
由于这里先写了,才能确保在把binlog写到磁盘后崩溃,可以恢复数据:若是判断到redo log是prepare状态,那么查看是否存XID对应的binlog,若是存在,则表示事务成功提交,须要用prepare状态的redo log进行恢复。
这样即便崩溃了,也能够经过redo log来进行恢复了,恢复流程以下:
Redo Log是循环写的,以下图:
LSN
,每当系统崩溃重启,都会从当前checkpoint这个位置执行重作日志,根据重作日志逐个确认数据页是否没问题,有问题就经过redo log进行修复。LSN Log Sequence Number的缩写。表明日志序列号。在InnoDB中,LSN占用8个字节,单调递增,LSN的含义:
- 重作日志写入的总量;
- checkpoint的位置;
- 页的版本;
除了重作日志中有LSN,每一个页的头部也是有存储了该页的LSN,咱们前面介绍页面格式的时候有介绍过。
在页中LSN表示该页最后刷新时LSN的大小。[19]
上面说的redo log记录了事务的行为,能够经过其对页进行重作操做,可是食物有时候须要进行回滚,这时候就须要undo log了。[20]
关于Undo Log的存储:InnoDB中有回滚段(rollback segment),每一个回滚段记录1024个undo log segment,在每一个undo log segment段中进行申请undo页。系统表空间偏移量为5的页记录了全部的rollback segment header所在的页。
根据行为不一样分为两种:
insert undo log
insert undo log
:只对事务自己可见,因此insert undo log在事务提交后可直接删除,无需执行purge操做;
insert undo log主要记录了:
next | 记录下一个undo log的位置 |
---|---|
type_cmpl | undo的类型:insert or update |
*undo_no | 记录事务的ID |
*table_id | 记录表对象 |
*len1, col1 | 记录列和值 |
*len2, col2 | 记录列和值 |
... | ... |
start | 记录undo log的开始位置 |
假设在事务1001中,执行如下sql,t20的table_id为10:
insert into t20(id, a, b, c, d) values(12, 2, 3, 1, "init")
那么对应会生成一条undo log:
update undo log
update undo log
:执行update或者delete会产生undo log,会影响已存在的记录,为了实现MVCC(后边介绍),update undo log不能再事务提交时马上删除,须要将事务提交时放入到history list上,等待purge线程进行最后的删除操做。
update undo log主要记录了:
next | 记录下一个undo log的位置 |
---|---|
type_cmpl | undo的类型:insert or update |
*undo_no | undo日志编号 |
*table_id | 记录表对象 |
info_bits | |
*DATA_TRX_ID | 事务的ID |
*DATA_ROLL_PTR | 回滚指针 |
*len1, i_col1 | n_unique_index |
*len2, i_col2 | |
... | |
n_update_fields | 如下是update vector信息,表示update操做致使发送改变的列 |
*pos1, *len1, u_old_col1 | |
*pos2, *len2, u_old_col2 | |
... | |
n_bytes_below | |
*pos, *len, col1 | |
*pos, *len, col2 | |
... | |
start | 记录undo log的开始位置 |
假设在事务1002中,执行如下sql,t20的table_id为10:
update t20 set d="update1" where id=60;
那么对应会生成一条undo log:
如上图,每回退应用一个undo log,就回退一个版本,这就是MVCC(Multi versioning concurrency control)的实现原理。
下面咱们在执行一个delete sql:
delete from t20 where id=60;
对应的undo log变为以下:
如上图,实际的行记录不会马上删除,而是在行记录头信息记录了一个deleted_flag
标志位。最终会在purge线程purge undo log的时候进行实际的删除操做,这个时候undo log也会清理掉。
如上图所示,MySQL只会有一个行记录,可是会把每次执行的sql致使行记录的变更,经过undo log的形式记录起来,undo log经过回滚指针链接在一块儿,这样咱们想回溯某一个版本的时候,就能够应用undo log,回到对应的版本视图了。
咱们知道InnoDB是支持RC
(Read Commit)和RR
(Repeatable Read)事务隔离级别的,而这个是经过一致性视图
(consistent read view)实现的。
一个事务开启瞬间,全部活跃的事务(未提交)构成了一个视图数组,InnoDB就是经过这个视图数组来判断行数据是否须要undo到指定的版本:
假设咱们使用了RR事务隔离级别。咱们看个例子:
以下图,假设id=60的记录a=1
事务C启动的瞬间,活跃的事务以下图黄色部分所示:
也就是对于事务A、事务B、事务C,他们可以看到的数据只有是行记录中的最大事务IDDATA_TRX_ID
<=11的,若是大于,那么只能经过undo进行回滚了。若是TRX_ID=当前事务id,也能够看到,即看到本身的改动。
另外有一个须要注意的:
因此咱们分析上面的例子的执行流程:
本文内容比较多,看完以后须要多梳理,最后你们能够对照着这个思惟导图回忆一下,这些内容是否都记住了:
这篇文章的内容就差很少介绍到这里了,可以阅读到这里的朋友真的是颇有耐心,为你点个赞。
本文为arthinking
基于相关技术资料和官方文档撰写而成,确保内容的准确性,若是你发现了有何错漏之处,烦请高抬贵手帮忙指正,万分感激。
你们能够关注个人博客:itzhai.com
获取更多文章,我将持续更新后端相关技术,涉及JVM、Java基础、架构设计、网络编程、数据结构、数据库、算法、并发编程、分布式系统等相关内容。
若是您以为读完本文有所收获的话,能够关注
个人帐号,或者点赞
吧,码字不易,您的支持就是我写做的最大动力,再次感谢!
关注个人公众号,及时获取最新的文章。
更多文章
本文做者: arthinking
版权声明:
BY-NC-SA
许可协议:创做不易,如需转载,请务必附加上博客连接,谢谢!
innodb_data_home_dir. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_data_home_dir ↩︎
ib_buffer_pool. Retrieved from https://dev.mysql.com/doc/refman/5.6/en/innodb-preload-buffer-pool.html ↩︎
ib_logfile0. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/innodb-redo-log.html ↩︎
ibtmp1. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/innodb-temporary-tablespace.html ↩︎
db.opt. Retrieved from https://dev.mysql.com/doc/refman/8.0/en/data-dictionary-file-removal.html ↩︎
Linux Programmer's Manual - OPEN(2). (2020-02-09). Retrieved from http://man7.org/linux/man-pages/man2/open.2.html ↩︎
man-pages.write. (2019-10-10). Retrieved from http://man7.org/linux/man-pages/man2/write.2.html ↩︎
man-pages.fdatasync. (2019-03-06). Retrieved from http://man7.org/linux/man-pages/man2/fdatasync.2.html ↩︎
On Disk IO, Part 1: Flavors of IO. medium.com. Retrieved from https://medium.com/databasss/on-disk-io-part-1-flavours-of-io-8e1ace1de017 ↩︎
Innodb calls fsync for writes with innodb_flush_method=O_DIRECT. Retrieved from https://bugs.mysql.com/bug.php?id=45892 ↩︎
14.6.3.3 General Tablespaces. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/general-tablespaces.html ↩︎
MYSQL INNODB表压缩. (2018-03-09). Retrieved from https://cloud.tencent.com/developer/article/1056453 ↩︎
前缀索引,一种优化索引大小的解决方案. (2015-03-03). Retrieved from https://www.cnblogs.com/studyzy/p/4310653.html ↩︎
MySQL Internals Manual - innodb page structure[EB/OL]. (2020-05-04). Retrieved 2020-0530, from https://dev.mysql.com/doc/internals/en/innodb-page-structure.html ↩︎
official.MySQL Internals Manual - innodb record structure[EB/OL]. (2020-05-04). Retrieved 2020-0530, from https://dev.mysql.com/doc/internals/en/innodb-record-structure.html ↩︎
姜承尧. MySQL技术内幕-InnoDB存储引擎第二版[M]. 机械工业出版社, 2013-5:104. ↩︎
为何 MySQL 使用 B+ 树. draveness.me. (2019-12-11). Retrieved from https://draveness.me/whys-the-design-mysql-b-plus-tree/ ↩︎
InnoDB DoubleWrite Buffer as Read Cache using SSDs∗. Retrieved from https://www.usenix.org/legacy/events/fast12/poster_descriptions/Kangdescription2-12-12.pdf ↩︎
姜承尧. MySQL技术内幕-InnoDB存储引擎第二版[M]. 机械工业出版社, 2013-5:302-303. ↩︎
姜承尧. MySQL技术内幕-InnoDB存储引擎第二版[M]. 机械工业出版社, 2013-5:306. ↩︎