系列文章:MySQL系列专栏mysql
InnoDB 存储引擎是MySQL的默认存储引擎,是事务安全的MySQL存储引擎。该存储引擎是第一个完整ACID事务的MySQL存储引擎,其特色是行锁设计、支持MVCC、支持外键、提供一致性非锁定读,同时被设计用来最有效地利用以及使用内存和 CPU。所以颇有必要学习下InnoDB存储引擎,它的不少架构设计思路均可以应用到咱们的应用系统设计中。程序员
咱们经过下面这张图先对 InnoDB 存储引擎的体系有一个总体的认识,里面有不少细节后面会分几篇文章来学习。web
好比要更新 user 表中 id=1 的这条数据,它的大体流程以下:sql
一、客户端链接到MySQL服务器,将SQL更新语句发送到服务器;MySQL服务器链接池中会有一个链接和客户端创建链接,而后后台线程会从链接中获取到要执行的SQL语句,并发送给SQL接口去调度执行。数据库
二、增、删、改 时,会将查询缓存中 user 表相关的缓存都清空。缓存
三、SQL语句通过SQL解析器解析、优化器优化,获得一个执行路径,前面这些和执行查询其实都是相似的。安全
四、接着由执行引擎去调用底层的存储引擎接口,根据执行计划完成SQL语句的执行。性能优化
① 首先查询出要更新的数据,这一步会先判断缓冲池(Buffer Pool)中是否已经存在这条数据,若是已经存在了,则直接从缓存池获取数据返回。不然从磁盘数据文件中加载这条数据到缓冲池中,再返回数据。服务器
② 获取到数据后,执行引擎会根据SQL更新数据,而后调用存储引擎更新数据。这一步会对数据加排它锁,避免并发更新问题。以后先写 undolog 到缓冲池,undolog 主要用于事务回滚、MVCC等;同时,undolog 也会产生 redolog 日志。markdown
③ 以后更新缓冲池中的数据,同时记录 redolog 到 RedoLog缓冲池,redolog 主要用于保证数据的持久性,宕机恢复数据等。
④ 最后提交事务,虽然没有手动 commit 提交事务,update 语句执行完成后也会有隐式的事务提交的。事务提交时,会先在MySQL服务器层面会写入 binlog,binlog是数据持久性的保证。最后将redolog刷入磁盘,完成事务提交。
五、最底层的一部分就是磁盘上的数据文件、日志文件等,能够看到,InnoDB 设计了缓冲池来缓冲数据、undolog、redolog 等,这些内存中的数据最终都是要刷新到磁盘中才能保证数据不丢失的。至于为何要这么设计,咱们后面再分析。
从上面的图中,咱们至少能够精炼出以下的核心内容:
MySQL相关的知识太多了,这个MySQL系列要学习的内容主要就包含上面的一些核心知识点,咱们主要就从这些点去研究下MySQL的一些优秀设计思想,而后可以对MySQL进行一些性能优化,就差很少了。
这篇文章咱们先看下MySQL和InnoDB的数据文件结构,为后面的研究打下基础。
咱们能够经过 datadir
这个系统变量查看MySQL的数据目录位置,默认是在 /var/lib/mysql
下。
mysql> show variables like 'datadir';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| datadir | /var/lib/mysql/ |
+---------------+-----------------+
复制代码
在一个全新安装的数据库的数据目录下,能够看到以下的一些初始化的文件和目录。
咱们能够重点关注 ibdata一、ib_logfile0、ib_logfile1
这几个文件,之后会讲到。ibdata1 是共享表空间,ib_logfile0、ib_logfile1 是 redo 日志文件。
还有一个 f4e2d8fde38c.pid
的文件,当MySQL实例启动时,会将本身的进程ID写入一个pid文件。该文件可由参数pid_file
控制,默认位于数据库目录下,文件名为主机名.pid
。
mysql> show variables like 'pid_file';
+---------------+---------------------------------+
| Variable_name | Value |
+---------------+---------------------------------+
| pid_file | /var/lib/mysql/f4e2d8fde38c.pid |
+---------------+---------------------------------+
复制代码
MySQL默认建立了四个系统数据库,除了 information_schema
,另外三个都会有一个目录与之对应。
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
复制代码
咱们经过 create database xx;
建立一个测试数据库,并指定了字符集为 utf8mb4
:
mysql> create database test default character set utf8mb4;
Query OK, 1 row affected (0.00 sec)
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
| test |
+--------------------+
复制代码
建立数据库后就会看到多了一个同名的目录,也就是说MySQL中的数据库在文件系统中其实就是数据目录下的一个子目录。
进入数据库目录下能够看到,建立数据库时会同步建立一个名为 db.opt
的文件,这个文件中包含了该数据库的各类属性,好比说该数据库默认的字符集和比较规则等。
前边提到了MySQL的几个系统数据库,下面简单看下每一个数据库都是干什么的。
mysql
这个数据库的核心,它存储了MySQL的用户帐户和权限信息,一些存储过程、事件的定义信息,一些运行过程当中产生的日志信息,一些帮助信息以及时区信息等。
information_schema
这个数据库保存着MySQL服务器全部其余数据库的信息,好比表、视图、触发器、列、索引、锁、事务等等。这些信息并非真实的用户数据,而是一些描述性信息,也称之为元数据。
performance_schema
这个数据库主要保存MySQL服务器运行过程当中的一些状态信息,包括统计最近执行了哪些语句,在执行过程的每一个阶段都花费了多长时间,内存的使用状况等等信息。
sys
这个数据库主要是经过视图的形式把 information_schema 和 performance_schema 结合起来,让程序员能够更方便的了解MySQL服务器的一些性能信息。
在 test 数据库下,先用下面的SQL建立一张 user
表,指定的存储引擎为 InnoDB:
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(60) NOT NULL COMMENT '用户名',
`nickname` varchar(240) DEFAULT NULL COMMENT '昵称',
`age` int(11) DEFAULT NULL COMMENT '年龄',
PRIMARY KEY (`id`),
UNIQUE KEY `user_uk_username` (`username`) USING BTREE
) ENGINE=InnoDB;
复制代码
建立完成以后就能够看到这张表了:
mysql> show tables;
+----------------+
| Tables_in_test |
+----------------+
| user |
+----------------+
复制代码
这个时候再看 test 目录,会发现多了两个文件:
user.frm
:表结构定义文件,格式为 表名.frm
user.ibd
:表空间文件,格式为 表名.ibd
不论表采用哪一种存储引擎,每张表都会有一个以.frm
为后缀名的文件,这个文件记录了该表的表结构定义。这个.frm
文件是以二进制格式存储的,直接打开会乱码。
InnoDB将数据按表空间(tablespace)进行存储,MySQL数据目录下名为ibdata1
的文件就是默认的表空间文件,也称为共享表空间。能够经过参数innodb_data_file_path
对其进行设置,格式以下:
innodb_data_file_path=datafile1[; datafile2]...
复制代码
能够经过多个文件组成一个表空间,同时制定文件的属性,如:
innodb_data_file_path=/db/ibdata1:2000M;/dr2/db/ibdata2:2000M:autoextend
复制代码
这里将 /db/ibdata1 和 /dr2/db/ibdata2 两个文件用来组成表空间,同时,两个文件的文件名后都跟了属性,表示文件 idbdata1 的大小为2000MB,文件ibdata2的大小为2000MB,若是用完了这2000MB,该文件能够自动增加(autoextend
)。
能够看到默认的 ibdata1 的大小为12M,且支持自动扩展。
mysql> show variables like 'innodb_data_file_path';
+-----------------------+------------------------+
| Variable_name | Value |
+-----------------------+------------------------+
| innodb_data_file_path | ibdata1:12M:autoextend |
+-----------------------+------------------------+
复制代码
若设置了参数innodb_file_per_table
,InnoDB每一个表将产生一个独立表空间。独立表空间的命名规则为 表名.ibd
,例如前面的 user.ibd
。这个配置默认是开启的,就是每张表都有一个独立的表空间文件来存储数据。
mysql> show variables like 'innodb_file_per_table';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| innodb_file_per_table | ON |
+-----------------------+-------+
复制代码
InnoDB将全部数据都存放在表空间中,表空间又由段(segment)、区(extent)、页(page)组成。InnoDB存储引擎的逻辑存储结构大体以下图。下面咱们就一个个来看看。
表空间能够看作是InnoDB存储引擎逻辑结构的最高层,全部的数据都存放在表空间中。在默认状况下InnoDB存储引擎有一个共享表空间ibdata1
,全部数据都存放在这个表空间内。
若是启用了参数innodb_file_per_table
,则每张表内的数据能够单独放到一个表空间内。须要注意的是,这些单独的表空间文件仅存储该表的数据、索引和插入缓冲Bitmap等信息,其他信息仍是存放在共享表空间中,例如 undo日志、插入缓冲索引页、系统事务信息、二次写缓冲等。
所以即便在启用了参数innodb_file_per_table
以后,共享表空间的大小仍是会不断地增长,例如事务中写入了undo日志,就算回滚了,共享表空间的大小也不会缩小。可是会判断这些undo信息是否还须要,不须要的话,就会将这些空间标记为可用空间,供下次重复使用。
InnoDB的数据是按行进行存放的,每一个页存放的行记录最多容许存放16KB / 2 -200
行的记录,即7992
行记录。
每行记录根据不一样的行格式、不一样的数据类型,会有不一样的存储方式。每行除了记录咱们保存的数据以外,还可能会记录事务ID(DB_TRX_ID),回滚指针(DB_ROLL_PTR)等。
页(Page)
是 InnoDB 磁盘管理的最小单位,默认每一个页的大小为16KB
,也就是最多能保证16KB的连续存储空间。
InnoDB 将数据划分为若干个页,以页做为磁盘和内存之间交互的基本单位,也就是一次最少从磁盘中读取一页16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。
InnoDB 为了避免同的目的设计了若干种不一样类型的页面,经常使用的页面类型有:
在介绍后面的内容前,这一小节先简单介绍下表中的数据是如何组织的,后面会单独一篇文章来说索引。
在InnoDB中,表都是根据主键顺序存放数据的,这种存储方式的表称为索引组织表
。在InnoDB表中,每张表都有个主键,若是在建立表时没有显式地定义主键,则InnoDB会按以下方式选择或建立主键:
row_id
的6字节的隐藏列做为主键。为了能快速的从磁盘中检索出数据,InnoDB采用 B+树
结构来组织数据,经过 B+树
组织起来的结构大概就像下图这个样子。B+树是多层的,B+树每一层中的页都会造成一个双向链表。在这棵树中,只有最底层的叶节点才存储数据,这些数据是按主键顺序存储的。上层的非叶子节点存储的则是索引目录,索引目录则根据主键区间划分了多个页和层级,这样就能够经过相似二分法的方式快速找到某条数据所在的页,而后经过主键定位到具体的某条数据。
能够看到,InnoDB存储引擎表是索引组织的,数据即索引,索引即数据。
在默认状况下,InnoDB存储引擎页的大小为16KB
,表空间中的页就太多了。为了更好的管理这些页,InnoDB 将物理位置上连续的64个页划为一个区
,任何状况下,每一个区的大小都为1MB
。
B+树中每一层都是经过双向链表链接起来的,若是是以页为单位来分配存储空间,原本链表中相邻的两个页之间的物理位置就可能离得很是远,那么磁盘查询时就会有大量的随机I/O,随机I/O是很是慢的。因此应该尽可能让链表中相邻的页的物理位置也相邻,这样能够消除不少的随机I/O,使用顺序I/O,尤为是在进行范围查询的时候。
因此在表中数据量大的时候,为某个索引分配空间的时候就再也不按照页为单位分配了,而是按照区为单位分配,甚至在表中的数据很是多的时候,能够一次性分配多个连续的区。
不管是系统表空间仍是独立表空间,均可以当作是由若干个区组成的,每一个区64个页,而后每256个区又被划分红一组。
第一个组最开始的3个页面的类型是固定的,也就是第一个区(extent0)最开始的三个页。分别是:
FSP_HDR
:用来登记整个表空间的一些总体属性以及本组全部区的属性,整个表空间只有一个 FSP_HDR 类型的页面。IBUF_BITMAP
:存储本组全部区的全部页面关于 INSERT BUFFER 的信息。INODE
:索引节点信息。其他各组则是最开始的2个页面的类型是固定的,分别是:
XDES
:用来登记本组256个区的属性。IBUF_BITMAP
:存储本组全部的区的全部页面关于 INSERT BUFFER 的信息。从这里也能够看出,索引数据并不时连续存储在区中,由于其中有些页面被用来存储额外的一些管理信息了。
从前面B+树的结构知道,B+树分为叶子节点和非叶子节点,最底层的叶子节点才存储了数据,非叶子节点是索引目录。若是将叶子节点页和非叶子节点页混合在一块儿存储,那在检索数据的时候一样也会有大量的随机I/O。
因此 InnoDB 又提出了段的概念,常见的段有数据段、索引段、回滚段等。段是一个逻辑上的概念,并不对应表空间中某一个连续的物理区域,它由若干个完整的区组成(还会包含一些碎片页),不一样的段不能使用同一个区。
存放叶子节点的区的集合就是数据段
,存放非叶子节点的区的集合就是索引段
。也就是说一个索引会生成2个段,一个叶子节点段(数据段),一个非叶子节点段(索引段)。