0%

MySQL本身也是在文件系统的基础上发展而来,因为锁的存在使之有所不同。

MySQL作为一种数据库软件,难免会存在对其共享资源并发访问,为了协调和管理不同资源的并发访问,也就产生了锁机制,因为锁机制的存在为数据库提供了数据的完整性一致性

锁的级别来分锁可分为:行级锁、表级锁、页级锁。
锁的类型来分锁可分为:共享锁、排它锁(独占锁)。
为了协调行锁、表锁产生了:意向锁(表级锁)。

共享锁,允许事务去读取数据。
排它锁,允许事务去修改或删除数据。
意向锁,获取行级锁的时候,自动添加的表级锁,包含:意向共享锁、意向排它锁。

对于MyISAM存储引擎,只支持表锁,而InnoDB存储引擎则支持行锁、表锁

MyISAM存储引擎修改、删除数据的时候,会产生排它锁,锁定的整张表,并发写入性能较差,而读取的时候产生的是共享锁,不会锁定表,读取性能就比较好。

InnoDB存储引擎修改、删除数据的时候,会产生排它锁,锁定的特定索引记录,一般不会影响表中的其它行,并发写入性能较好,而读取的时候产生的是共享锁,不会锁定表和行,读取性能较好。

行锁锁定的是索引记录,而不是记录行,如果没有索引,则使用隐式索引进行锁定。

当一张表某些行已经获取了排它锁,在表中会产生一个意向排它锁,如果此时有一个事务要来锁定整张表,那么一看有意向排它锁的存在,该事务就会被阻塞,通过意向锁直接就可以知道能不能锁定表,不需要逐行去遍历检测是否有排它锁,通过意向锁高效地协调了行锁和表锁的关系。

行级锁按照锁定范围来分,又分为三种:

  • Record Lock 单行记录上的锁。
  • Gap Lock 间隙锁,锁定一个范围,不包含记录本身。
  • Next-Key Lock 锁定一个范围,包含记录本身,用于解决幻读问题。

当然,锁也是有利有弊的,也可能出现死锁的情况。
两个或两个以上的事务在执行过程中,因争夺资源而造成一种相互等待的现象,称为死锁

最后,也是因为锁的存在,丰富了后续事务的功能。

MySQL通过设计一种机制,使得数据能够完整地从一种一致性状态切换到另一种一致性状态,这种机制称为事务。

事务包含有四大特性:原子性(A)、一致性(C)、隔离性(I)、持久性(D),简称酸性。
原子性:事务中的操作,要么全部成功,要么全部失败,不可切分。
一致性:事务将数据库从一种一致性状态转变成另外一种一致性状态,并且保证数据的完整性。
隔离性:又称并发控制,事务在提交之前对于其它事务是处于不可见的状态的。
持久性:事务一旦提交,结果就是永久性的,不会因为数据库宕机而丢失数据。

原子性持久性是通过redo日志实现的,一致性是通过undo日志实现的,隔离性是通过锁机制实现的。

从本质上来说,原子性也是为了配合持久性而存在的,当事务的一部分写入redo日志后,发生了崩溃、断电,那么根据原子性来说,该次事务应当恢复,那么对于已经持久化到日志文件中的数据,就必须要通过回溯来撤销。在InnoDB存储引擎中,redo重做日志对应的就是ib_logfile0ib_logfile1
redo

接着,事务要进行回滚,那就需要通过一致性来保障,而undo日志就是用来实现一致性的,在undo日志中保存了多个版本的事务的一些信息,通过undo日志,将事务rollback到修改之前的样子。

在此,不得不提的是MySQL的MVCC多版本并发控制,它也是通过undo日志来实现的。
MVCC是通过在每一数据行后头添加2个隐藏字段create versiondelete version以及每次开启一个事务会初始化一个事务id。新增一条数据的时候,create version的值就等于事务id,删除数据的时候,delete version就等于事务id,更新数据的时候会先删后增,在undo日志中就会存在2条数据,一条delete version就等于事务id,一条create version的值等于事务id

在事务执行过程中,可能会同时存在其它的事务,而多个事务之前需要相互隔离,也就是要做到并发控制,锁就是用来实现隔离性的。MySQL的事务的隔离级别包含:Read Uncommitted读未提交、Read Committed读已提交、Read Repeatable可重复读、Serializable串行化。其中,读已提交可重复读是基于MVCC多版本并发控制来实现的。

锁,为事务的并发控制带来了好处,同时也带来了坏处,包括:脏读、不可重复读、幻读。

脏读,指的是一个事务读到了另一个事务未提交的内容,一旦另一个事务回滚了,就出现了脏数据
不可重复读,指的是同一个事务使用同一句SQL进行多次读取,返回不同的结果。
幻读,指的是一个事务在进行增删的时候,某些已经确定不会出现的记录突然出现。

要解决脏读,那就需要至少设置隔离级别为:Read Committed读已提交。
要解决不可重复读,那就需要至少设置隔离级别为:Read Repeatable可重复读。
要解决幻读,那就需要设置隔离级别为:Serializable串行化或者采用Next-Key Lock间隙锁。

在MySQL数据库中,索引是一种很重要的提升查询性能的存储结构。

索引,就像是书本的目录一样,通过查询索引,可以快速的定位到数据所处位置,减少磁盘I/O次数,提高性能。
InnoDB存储引擎中,支持的索引包含:B+树全文索引哈希索引

本文将主要聚焦在由B+树实现的索引结构,全文索引基于倒排索引实现,哈希索引基于链表、哈希函数实现。

b+

首先,看一下二分查找,要从一个有序数组中查找一个元素,通过每次的折半再折半,时间复杂度为O(logn),如果直接顺序查找元素的话,时间复杂度为O(n)

mbs

接着,拓展到二叉查找树,如果二叉查找树足够平衡,那么时间复杂度还可以接近二分查找

mbst

反之,二叉查找树呈现线性状态的话,那么时间复杂度就接近于顺序查找了。

mbst2

因此,B+树既然能够提升查询性能的话,那必然是一棵平衡查找树,不能出现线性状态。
二叉查找树定义:左边节点值小于根节点值,右边节点值大于根节点值,中序遍历后为从小到大排序的列表。
平衡二叉树定义:在二叉查找树的条件下,又满足左右子树任何节点高度差不超过1。

B+树的所有记录节点都是按照顺序排列,从小到大排列在叶子节点,并且每个叶子节点之间有指针进行连接。

b+

B+树的叶子节点中,包含了所有的节点及数据,其它非叶子节点仅仅包含指针引用。

在执行范围查找的时候,只需要在B+树上进行二分查找,然后找到底层叶子节点的起始位置,再沿着叶子节点间的双向指针顺序遍历即可,一般B+树的高度在2 ~ 4之间。

除了这个优点之外,由于非叶子节点不存储数据,经过一次磁盘I/O操作,就可以加载大量的节点,然后在内存中高效执行二分查找,再去找到数据所在叶子节点即可。

由B+树来实现的索引包含:聚集索引、非聚集索引。

每张表有且仅有一个聚集索引,通常情况下,是采用主键来作为聚集索引。
聚集索引根据每张表的主键来构造B+树,在叶子节点则存储整张表的行记录数据
聚集索引是逻辑连续的,因此,在范围查找、根据主键排序的时候,效率特别高。

与聚集索引相反的是非聚集索引,又称为辅助索引,它的叶子节点并不存储行记录数据
每张表可以拥有多个非聚集索引,通过非聚集索引查找数据会遍历找到叶子节点,再通过叶子节点指向的聚集索引的指针,再去聚集索引上遍历然后查找对应的行记录数据,也因此通过非聚集索引查找数据比通过聚集索引慢了一倍的速度。

msindex

基于B+树两类索引实现的应用包含有:联合索引、覆盖索引。

索引既可以使用单独的列,又可以使用多个列组合起来使用,这就是联合索引
比如有这么一张表,包含有主键id及name、age组成的联合索引及一个addr普通字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mysql> desc test;
+-------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| id | int(11) | NO | PRI | 0 | |
| name | varchar(30) | YES | MUL | NULL | |
| age | int(11) | YES | | NULL | |
| addr | varchar(200) | YES | | NULL | |
+-------+--------------+------+-----+---------+-------+

mysql> show index from test;
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| test | 0 | PRIMARY | 1 | id | A | 0 | NULL | NULL | | BTREE | | |
| test | 1 | union_x | 1 | name | A | 0 | NULL | NULL | YES | BTREE | | |
| test | 1 | union_x | 2 | age | A | 0 | NULL | NULL | YES | BTREE | | |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+

当然,由于采用的B+树实现的,联合索引底层叶子节点的数据也是按多列顺序排列。
对于联合索引,需要符合最左原则,才可以使用到索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
mysql> explain select * from test where name = 1 and age = 1;
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| 1 | SIMPLE | test | ALL | union_x | NULL | NULL | NULL | 997182 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+

mysql> explain select * from test where age = 1 and name = 1;
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| 1 | SIMPLE | test | ALL | union_x | NULL | NULL | NULL | 997182 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+

mysql> explain select * from test where name = 1;
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| 1 | SIMPLE | test | ALL | union_x | NULL | NULL | NULL | 997182 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+

mysql> explain select * from test where age = 1;
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
| 1 | SIMPLE | test | ALL | NULL | NULL | NULL | NULL | 997182 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+

通过以上四个例子可以看到,联合索引如果按照其中一个列去查询的话,那就必须按照最左原则。
现在有联合索引<name,age>,通过name去查询可以用到索引,而通过age无法使用索引。
如果同时使用name和age的话,在查询的where条件后面可以调整name和age的顺序且不影响索引的使用。

在联合索引的基础上,又产生了一种覆盖索引,也就是直接从非聚集索引上就可以查到数据。

1
2
3
4
5
6
mysql>  explain select name,age from test where age = 1;
+----+-------------+-------+-------+---------------+---------+---------+------+--------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+------+--------+--------------------------+
| 1 | SIMPLE | test | index | NULL | union_x | 38 | NULL | 997182 | Using where; Using index |
+----+-------------+-------+-------+---------------+---------+---------+------+--------+--------------------------+

由于name、age组成了联合索引,通过非聚集索引(辅助索引)就可以找到name和age。
通过覆盖索引查询则无需再去聚集索引查询数据,可以减少磁盘I/O。

上文提到B+树高度一般在2 ~ 4层,这是由InnoDB存储引擎数据组织方式决定的。

磁盘中,最小的存储单元扇区,每一个扇区大小为512字节。
文件系统中,最小的存储单元是,每一个块大小为4KB。
InnoDB中,最小的存储单元是,每一页大小为16KB。

innodb-driver

在数据表中,数据都是存储在页上,一页16KB,如果一行数据1KB,那么一页可以存储16行数据。
假设主键大小为8字节,指针大小为6字节,则一个非叶子节点页可以存储16*1024/(8+6)=1170个单元。

如果高度为2,每个单元都指向一个页,那么可以存储1170*16=18720条记录。
如果高度为3,那么可以存储1170*1170*16=21902400条记录,大概2000万条。
如果高度为4,那么可以存储1170*1170*1170*16=25625808000条记录,大概200亿条。

一般来说,一张表数据量到了2000万条算多的了,不至于到200亿条。因此,B+树高度一般在2-4层,对于磁盘来说,1秒可以进行100次磁盘I/O操作,加载3层B+树,需要3次磁盘I/O,也就是0.03秒

在计算机通信中,I/O在数据交换中起到了至关重要的作用。    

最初,只有BIO这种同步阻塞式的I/O编程通信模型。
随着计算机普及及流量的上涨,产生了NIO这种同步非阻塞式的I/O编程通信模型。
再后来,NIO出现了瓶颈,便产生了AIO这种异步非阻塞式的I/O编程通信模型。

对于I/O类型,分为:文件I/O、网络I/O。

文件I/O:基本本地磁盘,在内核空间与用户进程空间之间交换数据。
网络I/O:基本网络Socket通信,在不同(主机)进程之间交换数据。

对于I/O方式,分为:同步、异步。

同步方式:后续的操作需要等待前面操作完成才可以继续执行。
异步方式:后续的操作不需要等待前面操作完成,可以直接返回,通过event、callback调用。

对于I/O状态,分为:阻塞、非阻塞。

阻塞状态:当前执行的线程将处于阻塞状态,无法继续执行其他的任务。
非阻塞状态:当前执行的线程不会处于阻塞状态,可以继续其它任务,I/O操作由后台去处理。

I/O通信模型围绕着上述方式和状态分为:BIO、NIO、AIO。

首先,是BIO,同步阻塞I/O,最为原始,设计最简单,适用于并发线程少于1000的情况。
在服务端,绑定IP、监听端口,启动一个Acceptor线程,阻塞等待accept。
当客户端进发起连接请求时,创建一个线程进行Socket连接,等待与客户端之间进行读写I/O流操作。
客户端使用连接完毕之后,断开连接,销毁线程。
当然,由于创建线程成本过高,可以采用线程池进行优化线程成本。
但是,如果线程内部I/O阻塞,线程池资源也会带来瓶颈,也因此无法同时承受太多并发连接。

linux-bio

os-bio

接着,是NIO,同步非阻塞I/O,底层基于Reactor模型来实现。
BIO不同的是,在NIO中,客户端与服务端的Channel建立连接后,由Selector线程不断的去轮询,获取当前就绪的Channel。
而不是来一个请求直接启动一个线程。Selector是一个多路复用器,Channel被注册到Selector上。
客户端只与Channel进行交互,所有的读写不是基于流,而是基于Buffer,有I/O操作时才会创建线程。
通过多路复用器的轮询,而不是Acceptor的阻塞accept,实现了非阻塞I/O。
对于多路复用器轮询出可用Channel的操作,根据操作系统实现又包含有:select、poll、epoll、kqueue。

linux-nio

os-nio

AIO作为NIO的升级版,是异步非阻塞I/O,底层基于Proactor模型来实现,适用高并发场景。
整体上基于事件和回调机制,文件通道、套接字通道都是异步的,读写返回的是Future
不需要再去注册相关感兴趣的key,只需要等待事件和I/O操作结果返回即可。

linux-aio

总的来说:
BIO,基于同步阻塞,适用连接较少且连接使用时间均匀的场景,耗费服务器资源较多。
NIO,基于同步非阻塞,适用连接较多且连接使用时间短的场景,充分利用服务器资源。
AIO,基于异步非阻塞,适用连接较多且连接使用时间长的场景,充分利用操作系统来完成并发操作。