页的数据结构:

数据页结构,页是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_ownednext_record 属性取出对应的记录,不过因为这一操作是在内存中进行的,所以通常会忽略这部分查找的耗时。

关于MySQL的索引:

索引是数据库中非常非常重要的概念,它是存储引擎能够快速定位记录的秘密武器,对于提升数据库的性能、减轻数据库服务器的负担有着非常重要的作用;索引优化是对查询性能优化的最有效手段,它能够轻松地将查询的性能提高几个数量级。需要注意的是查询记录时每次只能使用一个索引,因为和全表扫描和只是用一个索引的速度比起来,去分析两个索引二叉树更耗费时间。

那么索引是如何存储的呢?

InnoDB 存储引擎在绝大多数情况下使用 B+ 树建立索引,这是关系型数据库中查找最为常用和有效的索引,但是 B+ 树索引并不能找到一个给定键对应的具体值,它只能找到数据行对应的页,然后正如上一节所提到的,数据库把整个页读入到内存中,并在内存中查找具体的数据行。

B+ 树是平衡树,它查找任意节点所耗费的时间都是完全相同的,比较的次数就是 B+ 树的高度;在这里,我们并不会深入分析或者动手实现一个 B+ 树,只是对它的特性进行简单的介绍。

数据库中的 B+ 树索引可以分为聚集索引(clustered index)和辅助索引(secondary index),它们之间的最大区别就是,聚集索引中存放着一条行记录的全部信息,而辅助索引中只包含索引列和一个用于查找对应行记录的『书签』,在mysql中,可以把主键理解成聚集索引,如果没有创建,系统会自动创建一个隐含列为表的聚集索引。

MySQL的并发控制:

三种常见的并发控制机制:分别是悲观并发控制、乐观并发控制和多版本并发控制,其中悲观并发控制其实是最常见的并发控制机制,也就是锁;而乐观并发控制其实也有另一个名字:乐观锁,乐观锁其实并不是一种真实存在的锁,我们会在文章后面的部分中具体介绍;最后就是多版本并发控制(MVCC)了,与前两者对立的命名不同,MVCC 可以与前两者中的任意一种机制结合使用,以提高数据库的读性能。

  • 乐观锁是一种思想,它其实并不是一种真正的『锁』,它会先尝试对资源进行修改,在写回时判断资源是否进行了改变,如果没有发生改变就会写回,否则就会进行重试,在整个的执行过程中其实都没有对数据库进行加锁
  • 悲观锁就是一种真正的锁了,它会在获取资源前对资源进行加锁,确保同一时刻只有有限的线程能够访问该资源,其他想要尝试获取资源的操作都会进入等待状态,直到该线程完成了对资源的操作并且释放了锁后,其他线程才能重新操作资源;

乐观锁不会存在死锁的问题,但是由于更新后验证,所以当冲突频率重试成本较高时更推荐使用悲观锁,而需要非常高的响应速度并且并发量非常大的时候使用乐观锁就能较好的解决问题,在这时使用悲观锁就可能出现严重的性能问题;在选择并发控制机制时,需要综合考虑上面的四个方面(冲突频率、重试成本、响应速度和并发量)进行选择。

共享锁和排它锁是悲观锁的不同的实现,共享锁和排它锁实现了标准的行级锁,它俩都属于悲观锁的范畴。 

 

锁的算法:

 

  • 共享锁(读锁):允许事务对一条行数据进行读取;
  • 互斥锁(写锁):允许事务对一条行数据进行删除或更新;

Record 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 不知道待修改的记录具体存放的位置,也无法对将要修改哪条记录提前做出判断就会锁定整个表。

Gap Lock

记录锁是在存储引擎中最为常见的锁,除了记录锁之外,InnoDB 中还存在间隙锁(Gap Lock),间隙锁是对索引记录中的一段连续区域的锁;当使用类似 SELECT * FROM users WHERE id BETWEEN 10 AND 20 FOR UPDATE; 的 SQL 语句时,就会阻止其他事务向表中插入 id = 15 的记录,因为整个范围都被间隙锁锁定了。

间隙锁是存储引擎对于性能和并发做出的权衡,并且只用于某些事务隔离级别。

虽然间隙锁中也分为共享锁和互斥锁,不过它们之间并不是互斥的,也就是不同的事务可以同时持有一段相同范围的共享锁和互斥锁,它唯一阻止的就是其他事务向这个范围中添加新的记录

Next-Key Lock

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] 范围内的记录都会被锁定。

事务:

在介绍了锁之后,我们再来谈谈数据库中一个非常重要的概念 —— 事务;相信只要是一个合格的软件工程师就对事务的特性有所了解,其中被人经常提起的就是事务的原子性,在数据提交工作时,要么保证所有的修改都能够提交,要么就所有的修改全部回滚。

但是事务还遵循包括原子性在内的 ACID 四大特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability);文章不会对这四大特性全部展开进行介绍,相信你能够通过 Google 和数据库相关的书籍轻松获得有关它们的概念,本文最后要介绍的就是事务的四种隔离级别。

几种隔离级别

事务的隔离性是数据库处理数据的几大基础之一,而隔离级别其实就是提供给用户用于在性能和可靠性做出选择和权衡的配置项。

ISO 和 ANIS SQL 标准制定了四种事务隔离级别,而 InnoDB 遵循了 SQL:1992 标准中的四种隔离级别:READ UNCOMMITEDREAD COMMITEDREPEATABLE READ 和 SERIALIZABLE;每个事务的隔离级别其实都比上一级多解决了一个问题:

  • RAED UNCOMMITED:使用查询语句不会加锁,可能会读到未提交的行(Dirty Read);
  • READ COMMITED:只对记录加记录锁,而不会在记录之间加间隙锁,所以允许新的记录插入到被锁定记录的附近,所以再多次使用查询语句时,可能得到不同的结果(Non-Repeatable Read);
  • REPEATABLE READ:多次读取同一范围的数据会返回第一次查询的快照,不会返回不同的数据行,但是可能发生幻读(Phantom Read);
  • SERIALIZABLE:InnoDB 隐式地将全部的查询语句加上共享锁,解决了幻读的问题;

MySQL 中默认的事务隔离级别就是 REPEATABLE READ,但是它通过 Next-Key 锁也能够在某种程度上解决幻读的问题。

总结:

          MySQL的Innodb存储引擎使用B+Tree来建立索引,索引可分为两大类,聚集索引和辅助索引,它们主要的区别是聚集索引(可以理解为主键)中存储了行记录的全部信息,在使用聚集索引检索数据时,可以直接查询到,而辅助索引值存储了索引中标明的列和一个用于查找对应行记录的书签(主键),如果需要查询完整的行记录,就要先通过辅助索引查找到主键,然后在聚集索引中使用主键查找完整的行记录。

          MySQL的并发控制有三种常见的机制:悲观控制、乐观控制和多版本控制,其中悲观并发控制其实是最常见的并发控制机制,也就是锁,分为行级锁和表锁,行级锁又分为共享锁和排它锁,事务其实是并发控制的基本单位,事务的隔离性是数据库处理数据的几大基础之一,分为脏读、不可重复读、可重复读、串行化,MySQL通过隔离性和锁来控制事务并发的可靠性。

参考资料:https://draveness.me/mysql-innodb

Logo

更多推荐