做为一名开发人员,在平常的工做中会难以免地接触到数据库,不管是基于文件的 sqlite 仍是工程上使用很是普遍的 MySQL、PostgreSQL,可是一直以来也没有对数据库有一个很是清晰而且成体系的认知,因此最近两个月的时间看了几本数据库相关的书籍而且阅读了 MySQL 的官方文档,但愿对各位了解数据库的、不了解数据库的有所帮助。html
本文中对于数据库的介绍以及研究都是在 MySQL 上进行的,若是涉及到了其余数据库的内容或者实现会在文中单独指出。mysql
不少开发者在最开始时其实都对数据库有一个比较模糊的认识,以为数据库就是一堆数据的集合,可是实际却比这复杂的多,数据库领域中有两个词很是容易混淆,也就是数据库和实例:git
对于数据库和实例的定义都来自于 MySQL 技术内幕:InnoDB 存储引擎 一书,想要了解 InnoDB 存储引擎的读者能够阅读这本书籍。github
在 MySQL 中,实例和数据库每每都是一一对应的,而咱们也没法直接操做数据库,而是要经过数据库实例来操做数据库文件,能够理解为数据库实例是数据库为上层提供的一个专门用于操做的接口。算法
在 Unix 上,启动一个 MySQL 实例每每会产生两个进程,mysqld
就是真正的数据库服务守护进程,而 mysqld_safe
是一个用于检查和设置 mysqld
启动的控制程序,它负责监控 MySQL 进程的执行,当 mysqld
发生错误时,mysqld_safe
会对其状态进行检查并在合适的条件下重启。sql
MySQL 从第一个版本发布到如今已经有了 20 多年的历史,在这么多年的发展和演变中,整个应用的体系结构变得愈来愈复杂:数据库
最上层用于链接、线程处理的部分并非 MySQL 『发明』的,不少服务都有相似的组成部分;第二层中包含了大多数 MySQL 的核心服务,包括了对 SQL 的解析、分析、优化和缓存等功能,存储过程、触发器和视图都是在这里实现的;而第三层就是 MySQL 中真正负责数据的存储和提取的存储引擎,例如:InnoDB、MyISAM 等,文中对存储引擎的介绍都是对 InnoDB 实现的分析。编程
在整个数据库体系结构中,咱们可使用不一样的存储引擎来存储数据,而绝大多数存储引擎都以二进制的形式存储数据;这一节会介绍 InnoDB 中对数据是如何存储的。缓存
在 InnoDB 存储引擎中,全部的数据都被逻辑地存放在表空间中,表空间(tablespace)是存储引擎中最高的存储逻辑单位,在表空间的下面又包括段(segment)、区(extent)、页(page):安全
同一个数据库实例的全部表空间都有相同的页大小;默认状况下,表空间中的页大小都为 16KB,固然也能够经过改变 innodb_page_size
选项对默认大小进行修改,须要注意的是不一样的页大小最终也会致使区大小的不一样:
从图中能够看出,在 InnoDB 存储引擎中,一个区的大小最小为 1MB,页的数量最少为 64 个。
MySQL 使用 InnoDB 存储表时,会将表的定义和数据索引等信息分开存储,其中前者存储在 .frm
文件中,后者存储在 .ibd
文件中,这一节就会对这两种不一样的文件分别进行介绍。
不管在 MySQL 中选择了哪一个存储引擎,全部的 MySQL 表都会在硬盘上建立一个 .frm
文件用来描述表的格式或者说定义;.frm
文件的格式在不一样的平台上都是相同的。
CREATE TABLE test_frm( column1 CHAR(5), column2 INTEGER );
当咱们使用上面的代码建立表时,会在磁盘上的 datadir
文件夹中生成一个 test_frm.frm
的文件,这个文件中就包含了表结构相关的信息:
MySQL 官方文档中的 11.1 MySQL .frm File Format 一文对于
.frm
文件格式中的二进制的内容有着很是详细的表述,在这里就不展开介绍了。
InnoDB 中用于存储数据的文件总共有两个部分,一是系统表空间文件,包括 ibdata1
、ibdata2
等文件,其中存储了 InnoDB 系统信息和用户数据库表数据和索引,是全部表公用的。
当打开 innodb_file_per_table
选项时,.ibd
文件就是每个表独有的表空间,文件存储了当前表的数据和相关的索引数据。
与现有的大多数存储引擎同样,InnoDB 使用页做为磁盘管理的最小单位;数据在 InnoDB 存储引擎中都是按行存储的,每一个 16KB 大小的页中能够存放 2-200 行的记录。
当 InnoDB 存储数据时,它可使用不一样的行格式进行存储;MySQL 5.7 版本支持如下格式的行存储方式:
Antelope 是 InnoDB 最开始支持的文件格式,它包含两种行格式 Compact 和 Redundant,它最开始并无名字;Antelope 的名字是在新的文件格式 Barracuda 出现后才起的,Barracuda 的出现引入了两种新的行格式 Compressed 和 Dynamic;InnoDB 对于文件格式都会向前兼容,而官方文档中也对以后会出现的新文件格式预先定义好了名字:Cheetah、Dragon、Elk 等等。
两种行记录格式 Compact 和 Redundant 在磁盘上按照如下方式存储:
Compact 和 Redundant 格式最大的不一样就是记录格式的第一个部分;在 Compact 中,行记录的第一部分倒序存放了一行数据中列的长度(Length),而 Redundant 中存的是每一列的偏移量(Offset),从整体上上看,Compact 行记录格式相比 Redundant 格式可以减小 20% 的存储空间。
当 InnoDB 使用 Compact 或者 Redundant 格式存储极长的 VARCHAR 或者 BLOB 这类大对象时,咱们并不会直接将全部的内容都存放在数据页节点中,而是将行数据中的前 768 个字节存储在数据页中,后面会经过偏移量指向溢出页。
可是当咱们使用新的行记录格式 Compressed 或者 Dynamic 时都只会在行记录中保存 20 个字节的指针,实际的数据都会存放在溢出页面中。
固然在实际存储中,可能会对不一样长度的 TEXT 和 BLOB 列进行优化,不过这就不是本文关注的重点了。
想要了解更多与 InnoDB 存储引擎中记录的数据格式的相关信息,能够阅读 InnoDB Record Structure
页是 InnoDB 存储引擎管理数据的最小磁盘单位,而 B-Tree 节点就是实际存放表中数据的页面,咱们在这里将要介绍页是如何组织和存储记录的;首先,一个 InnoDB 页有如下七个部分:
每个页中包含了两对 header/trailer:内部的 Page Header/Page Directory 关心的是页的状态信息,而 Fil Header/Fil Trailer 关心的是记录页的头信息。
在页的头部和尾部之间就是用户记录和空闲空间了,每个数据页中都包含 Infimum 和 Supremum 这两个虚拟的记录(能够理解为占位符),Infimum 记录是比该页中任何主键值都要小的值,Supremum 是该页中的最大值:
User Records 就是整个页面中真正用于存放行记录的部分,而 Free Space 就是空余空间了,它是一个链表的数据结构,为了保证插入和删除的效率,整个页面并不会按照主键顺序对全部记录进行排序,它会自动从左侧向右寻找空白节点进行插入,行记录在物理存储上并非按照顺序的,它们之间的顺序是由 next_record
这一指针控制的。
B+ 树在查找对应的记录时,并不会直接从树中找出对应的行记录,它只能获取记录所在的页,将整个页加载到内存中,再经过 Page Directory 中存储的稀疏索引和 n_owned
、next_record
属性取出对应的记录,不过由于这一操做是在内存中进行的,因此一般会忽略这部分查找的耗时。
InnoDB 存储引擎中对数据的存储是一个很是复杂的话题,这一节中也只是对表、行记录以及页面的存储进行必定的分析和介绍,虽然做者相信这部分知识对于大部分开发者已经足够了,可是想要真正消化这部份内容还须要不少的努力和实践。
索引是数据库中很是很是重要的概念,它是存储引擎可以快速定位记录的秘密武器,对于提高数据库的性能、减轻数据库服务器的负担有着很是重要的做用;索引优化是对查询性能优化的最有效手段,它可以轻松地将查询的性能提升几个数量级。
在上一节中,咱们谈了行记录的存储和页的存储,在这里咱们就要从更高的层面看 InnoDB 中对于数据是如何存储的;InnoDB 存储引擎在绝大多数状况下使用 B+ 树创建索引,这是关系型数据库中查找最为经常使用和有效的索引,可是 B+ 树索引并不能找到一个给定键对应的具体值,它只能找到数据行对应的页,而后正如上一节所提到的,数据库把整个页读入到内存中,并在内存中查找具体的数据行。
B+ 树是平衡树,它查找任意节点所耗费的时间都是彻底相同的,比较的次数就是 B+ 树的高度;在这里,咱们并不会深刻分析或者动手实现一个 B+ 树,只是对它的特性进行简单的介绍。
数据库中的 B+ 树索引能够分为汇集索引(clustered index)和辅助索引(secondary index),它们之间的最大区别就是,汇集索引中存放着一条行记录的所有信息,而辅助索引中只包含索引列和一个用于查找对应行记录的『书签』。
InnoDB 存储引擎中的表都是使用索引组织的,也就是按照键的顺序存放;汇集索引就是按照表中主键的顺序构建一颗 B+ 树,并在叶节点中存放表中的行记录数据。
CREATE TABLE users( id INT NOT NULL, first_name VARCHAR(20) NOT NULL, last_name VARCHAR(20) NOT NULL, age INT NOT NULL, PRIMARY KEY(id), KEY(last_name, first_name, age) KEY(first_name) );
若是使用上面的 SQL 在数据库中建立一张表,B+ 树就会使用 id
做为索引的键,并在叶子节点中存储一条记录中的全部信息。
图中对 B+ 树的描述与真实状况下 B+ 树中的数据结构有一些差异,不过这里想要表达的主要意思是:汇集索引叶节点中保存的是整条行记录,而不是其中的一部分。
汇集索引与表的物理存储方式有着很是密切的关系,全部正常的表应该有且仅有一个汇集索引(绝大多数状况下都是主键),表中的全部行记录数据都是按照汇集索引的顺序存放的。
当咱们使用汇集索引对表中的数据进行检索时,能够直接得到汇集索引所对应的整条行记录数据所在的页,不须要进行第二次操做。
数据库将全部的非汇集索引都划分为辅助索引,可是这个概念对咱们理解辅助索引并无什么帮助;辅助索引也是经过 B+ 树实现的,可是它的叶节点并不包含行记录的所有数据,仅包含索引中的全部键和一个用于查找对应行记录的『书签』,在 InnoDB 中这个书签就是当前记录的主键。
辅助索引的存在并不会影响汇集索引,由于汇集索引构成的 B+ 树是数据实际存储的形式,而辅助索引只用于加速数据的查找,因此一张表上每每有多个辅助索引以此来提高数据库的性能。
一张表必定包含一个汇集索引构成的 B+ 树以及若干辅助索引的构成的 B+ 树。
若是在表 users
中存在一个辅助索引 (first_name, age)
,那么它构成的 B+ 树大体就是上图这样,按照 (first_name, age)
的字母顺序对表中的数据进行排序,当查找到主键时,再经过汇集索引获取到整条行记录。
上图展现了一个使用辅助索引查找一条表记录的过程:经过辅助索引查找到对应的主键,最后在汇集索引中使用主键获取对应的行记录,这也是一般状况下行记录的查找方式。
索引的设计实际上是一个很是重要的内容,同时也是一个很是复杂的内容;索引的设计与建立对于提高数据库的查询性能相当重要,不过这不是本文想要介绍的内容,有关索引的设计与优化能够阅读 数据库索引设计与优化 一书,书中提供了一种很是科学合理的方法可以帮助咱们在数据库中创建最适合的索引,固然做者也可能会在以后的文章中对索引的设计进行简单的介绍和分析。
咱们都知道锁的种类通常分为乐观锁和悲观锁两种,InnoDB 存储引擎中使用的就是悲观锁,而按照锁的粒度划分,也能够分红行锁和表锁。
乐观锁和悲观锁其实都是并发控制的机制,同时它们在原理上就有着本质的差异;
虽然乐观锁和悲观锁在本质上并非同一种东西,一个是一种思想,另外一个是一种真正的锁,可是它们都是一种并发控制机制。
乐观锁不会存在死锁的问题,可是因为更新后验证,因此当冲突频率和重试成本较高时更推荐使用悲观锁,而须要很是高的响应速度而且并发量很是大的时候使用乐观锁就能较好的解决问题,在这时使用悲观锁就可能出现严重的性能问题;在选择并发控制机制时,须要综合考虑上面的四个方面(冲突频率、重试成本、响应速度和并发量)进行选择。
对数据的操做其实只有两种,也就是读和写,而数据库在实现锁时,也会对这两种操做使用不一样的锁;InnoDB 实现了标准的行级锁,也就是共享锁(Shared Lock)和互斥锁(Exclusive Lock);共享锁和互斥锁的做用其实很是好理解:
而它们的名字也暗示着各自的另一个特性,共享锁之间是兼容的,而互斥锁与其余任意锁都不兼容:
稍微对它们的使用进行思考就能想明白它们为何要这么设计,由于共享锁表明了读操做、互斥锁表明了写操做,因此咱们能够在数据库中并行读,可是只能串行写,只有这样才能保证不会发生线程竞争,实现线程安全。
不管是共享锁仍是互斥锁其实都只是对某一个数据行进行加锁,InnoDB 支持多种粒度的锁,也就是行锁和表锁;为了支持多粒度锁定,InnoDB 存储引擎引入了意向锁(Intention Lock),意向锁就是一种表级锁。
与上一节中提到的两种锁的种类类似的是,意向锁也分为两种:
随着意向锁的加入,锁类型之间的兼容矩阵也变得越发复杂:
意向锁其实不会阻塞全表扫描以外的任何请求,它们的主要目的是为了表示是否有人请求锁定表中的某一行数据。
有的人可能会对意向锁的目的并非彻底的理解,咱们在这里能够举一个例子:若是没有意向锁,当已经有人使用行锁对表中的某一行进行修改时,若是另一个请求要对全表进行修改,那么就须要对全部的行是否被锁定进行扫描,在这种状况下,效率是很是低的;不过,在引入意向锁以后,当有人使用行锁对表中的某一行进行修改以前,会先为表添加意向互斥锁(IX),再为行记录添加互斥锁(X),在这时若是有人尝试对全表进行修改就不须要判断表中的每一行数据是否被加锁了,只须要经过等待意向互斥锁被释放就能够了。
到目前为止已经对 InnoDB 中锁的粒度有必定的了解,也清楚了在对数据库进行读写时会获取不一样的锁,在这一小节将介绍锁是如何添加到对应的数据行上的,咱们会分别介绍三种锁的算法:Record Lock、Gap Lock 和 Next-Key Lock。
记录锁(Record Lock)是加到索引记录上的锁,假设咱们存在下面的一张表 users
:
CREATE TABLE users( id INT NOT NULL AUTO_INCREMENT, last_name VARCHAR(255) NOT NULL, first_name VARCHAR(255), age INT, PRIMARY KEY(id), KEY(last_name), KEY(age) );
若是咱们使用 id
或者 last_name
做为 SQL 中 WHERE
语句的过滤条件,那么 InnoDB 就能够经过索引创建的 B+ 树找到行记录并添加索引,可是若是使用 first_name
做为过滤条件时,因为 InnoDB 不知道待修改的记录具体存放的位置,也没法对将要修改哪条记录提早作出判断就会锁定整个表。
记录锁是在存储引擎中最为常见的锁,除了记录锁以外,InnoDB 中还存在间隙锁(Gap Lock),间隙锁是对索引记录中的一段连续区域的锁;当使用相似 SELECT * FROM users WHERE id BETWEEN 10 AND 20 FOR UPDATE;
的 SQL 语句时,就会阻止其余事务向表中插入 id = 15
的记录,由于整个范围都被间隙锁锁定了。
间隙锁是存储引擎对于性能和并发作出的权衡,而且只用于某些事务隔离级别。
虽然间隙锁中也分为共享锁和互斥锁,不过它们之间并非互斥的,也就是不一样的事务能够同时持有一段相同范围的共享锁和互斥锁,它惟一阻止的就是其余事务向这个范围中添加新的记录。
Next-Key 锁相比前二者就稍微有一些复杂,它是记录锁和记录前的间隙锁的结合,在 users
表中有如下记录:
+------|-------------|--------------|-------+ | id | last_name | first_name | age | |------|-------------|--------------|-------| | 4 | stark | tony | 21 | | 1 | tom | hiddleston | 30 | | 3 | morgan | freeman | 40 | | 5 | jeff | dean | 50 | | 2 | donald | trump | 80 | +------|-------------|--------------|-------+
若是使用 Next-Key 锁,那么 Next-Key 锁就能够在须要的时候锁定如下的范围:
(-∞, 21] (21, 30] (30, 40] (40, 50] (50, 80] (80, ∞)
既然叫 Next-Key 锁,锁定的应该是当前值和后面的范围,可是实际上却不是,Next-Key 锁锁定的是当前值和前面的范围。
当咱们更新一条记录,好比 SELECT * FROM users WHERE age = 30 FOR UPDATE;
,InnoDB 不只会在范围 (21, 30]
上加 Next-Key 锁,还会在这条记录后面的范围 (30, 40]
加间隙锁,因此插入 (21, 40]
范围内的记录都会被锁定。
Next-Key 锁的做用实际上是为了解决幻读的问题,咱们会在下一节谈事务的时候具体介绍。
既然 InnoDB 中实现的锁是悲观的,那么不一样事务之间就可能会互相等待对方释放锁形成死锁,最终致使事务发生错误;想要在 MySQL 中制造死锁的问题其实很是容易:
两个会话都持有一个锁,而且尝试获取对方的锁时就会发生死锁,不过 MySQL 也能在发生死锁时及时发现问题,并保证其中的一个事务可以正常工做,这对咱们来讲也是一个好消息。
在介绍了锁以后,咱们再来谈谈数据库中一个很是重要的概念 —— 事务;相信只要是一个合格的软件工程师就对事务的特性有所了解,其中被人常常提起的就是事务的原子性,在数据提交工做时,要么保证全部的修改都可以提交,要么就全部的修改所有回滚。
可是事务还遵循包括原子性在内的 ACID 四大特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability);文章不会对这四大特性所有展开进行介绍,相信你可以经过 Google 和数据库相关的书籍轻松得到有关它们的概念,本文最后要介绍的就是事务的四种隔离级别。
事务的隔离性是数据库处理数据的几大基础之一,而隔离级别其实就是提供给用户用于在性能和可靠性作出选择和权衡的配置项。
ISO 和 ANIS SQL 标准制定了四种事务隔离级别,而 InnoDB 遵循了 SQL:1992 标准中的四种隔离级别:READ UNCOMMITED
、READ COMMITED
、REPEATABLE READ
和 SERIALIZABLE
;每一个事务的隔离级别其实都比上一级多解决了一个问题:
RAED UNCOMMITED
:使用查询语句不会加锁,可能会读到未提交的行(Dirty Read);READ COMMITED
:只对记录加记录锁,而不会在记录之间加间隙锁,因此容许新的记录插入到被锁定记录的附近,因此再屡次使用查询语句时,可能获得不一样的结果(Non-Repeatable Read);REPEATABLE READ
:屡次读取同一范围的数据会返回第一次查询的快照,不会返回不一样的数据行,可是可能发生幻读(Phantom Read);SERIALIZABLE
:InnoDB 隐式地将所有的查询语句加上共享锁,解决了幻读的问题;MySQL 中默认的事务隔离级别就是 REPEATABLE READ
,可是它经过 Next-Key 锁也可以在某种程度上解决幻读的问题。
接下来,咱们将数据库中建立以下的表并经过个例子来展现在不一样的事务隔离级别之下,会发生什么样的问题:
CREATE TABLE test( id INT NOT NULL, UNIQUE(id) );
当事务的隔离级别为 READ UNCOMMITED
时,咱们在 SESSION 2
中插入的未提交数据在 SESSION 1
中是能够访问的。
当事务的隔离级别为 READ COMMITED
时,虽然解决了脏读的问题,可是若是在 SESSION 1
先查询了一个范围的数据,在这以后 SESSION 2
中插入一条数据而且提交了修改,在这时,若是 SESSION 1
中再次使用相同的查询语句,就会发现两次查询的结果不同。
不可重复读的缘由就是,在 READ COMMITED
的隔离级别下,存储引擎不会在查询记录时添加间隙锁,锁定 id < 5
这个范围。
从新开启了两个会话 SESSION 1
和 SESSION 2
,在 SESSION 1
中咱们查询全表的信息,没有获得任何记录;在 SESSION 2
中向表中插入一条数据并提交;因为 REPEATABLE READ
的缘由,再次查询全表的数据时,咱们得到到的仍然是空集,可是在向表中插入一样的数据却出现了错误。
这种现象在数据库中就被称做幻读,虽然咱们使用查询语句获得了一个空的集合,可是插入数据时却获得了错误,好像以前的查询是幻觉同样。
在标准的事务隔离级别中,幻读是由更高的隔离级别 SERIALIZABLE
解决的,可是它也能够经过 MySQL 提供的 Next-Key 锁解决:
REPERATABLE READ
和 READ UNCOMMITED
实际上是矛盾的,若是保证了前者就看不到已经提交的事务,若是保证了后者,就会致使两次查询的结果不一样,MySQL 为咱们提供了一种折中的方式,可以在 REPERATABLE READ
模式下加锁访问已经提交的数据,其自己并不能解决幻读的问题,而是经过文章前面提到的 Next-Key 锁来解决。
文章中的内容大都来自于 高性能 MySQL、MySQL 技术内幕:InnoDB 存储引擎、数据库索引设计与优化以及 MySQL 的 官方文档。
因为篇幅所限仅能对数据库中一些重要内容进行简单的介绍和总结,文中内容不免有所疏漏,若是对文章内容的有疑问,能够在博客下面评论留言。
原文连接:『浅入浅出』MySQL 和 InnoDB · 面向信仰编程
Follow: Draveness · GitHub