MySQL高级
前言:
- 本篇文章是本人学习MySQL高级的笔记。
- 资料:《MySQL是怎样运行的》、《小林Coding-图解MySQL》、《MySQL45讲》、《尚硅谷康师傅MySQL视频》
一、基础篇
1. 什么是关系型数据库?
关系型数据库是指采用了关系模型来组织数据的数据库,其以行和列的形式存储数据,一系列的行和列被称为表,一组表组成了数据库,关系模型表明了数据库中所存储的数据之间的联系(一对一、一对多、多对多)。 关系模型可以简单理解为二维表格模型,而一个关系型数据库就是由二维表及其之间的关系组成的一个数据组织。
关系型数据库(二维表格+关系)---关系模型---理解为二维表格模型---一行---一列---多行多列构成表---多张表构成库---此外关系模型还表明数据库中所存储的数据之间的联系(一对一、一对多、多对多)
关系型数据库是指采用了关系模型来组织数据的数据库,关系模型可以理解为二维表格模型,表中的一行称为一条记录或者元组,表中的一列称为字段或者属性,多行多列构成一张二维表,多张表构成数据库,此外关系模型还表明数据库中所存储的数据之间的联系(一对一、一对多、多对多),所以关系型数据库就是由二维表及其之间的关系组成了一个数据组织。
2. 🎯一条SQL查询语句的执行流程
MySQL内部架构 MySQL的架构可以分成3层:连接层、Server层和存储引擎层
连接层
- 连接器:
- 与客户端进行TCP三次握手建立连接
- 校验客户端的用户名和密码,如果用户名和密码不对,则会报错"Access denied for user",然后客户端程序结束执行
- 如果用户名和密码都对了,连接器会到权限表里面查出你拥有的权限,后面的逻辑判断都基于此时读取到的权限
数据库长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接。短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个。(尽量使用长连接)
服务层
- 查询缓存:如果是查询语句(查询请求),MySQL就会先去查询缓存里查询缓存数据,看之前有没有执行过一模一样的命令,如果查询缓存命中,则返回缓存结果给客户端。如果查询的语句没有命中查询缓存,那么就会继续往下执行,等执行完后,查询的结果就会被存入查询缓存中。这个查询缓存是以key-value的形式保存在内存中的,key为SQL查询语句,value为SQL语句查询的结果。(鸡肋,查询缓存的失效非常频繁,MySQL8.0已删除)
如果没有命中缓存,就需要真正执行SQL语句了。
- 解析器:
- 词法分析:分析SQL语句要干嘛
- 语法分析:分析SQL语句是否符合MySQL语法
最终会生成一个语法树,方便后序模块读取表名、字段、语句类型。
- 优化器:
- MySQL优化器会将SQL语句最优的执行方案确定下来,比如在表里面有多个索引的时候,优化器会基于查询成本的考虑,来决定选择使用哪个索引。
- 优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联的时候,决定各个表的连接顺序
- 执行器:
- 执行前查看用户是否有权限,有的话就会根据表的引擎定义,调用存储引擎API对表进行读写。存储引擎API只是抽象接口,下面还有个存储引擎层,具体实现开始要看表选择的存储引擎。
SELECT * FROM T WHERE ID = 10;
执行器的执行流程是这样的:
- 调用InnoDB引擎接口取这个表的第一行,判断ID值是不是10,如果不是则跳过,如果是则将这行存在结果集中
- 调用引擎接口取"下一行",重复相同的判断逻辑,直到取到这个表的最后一行。
执行器将上述遍历过程中所有满足条件的行组成的记录的结果集返回给客户端。
存储引擎层
略
MySQL的架构可以分为3层:连接层、服务层、存储引擎层
客户端执行一条SQL,首先需要通过TCP和服务端建立连接,验证用户名密码,读取用户权限。然后会查询缓存(前提是SELECT语句),如果缓存命中,则直接返回查询结果给客户端。如果未命中,则进入解析器中,进行SQL的词法分析(分析要干什么)、语法分析(分析语法是否正确)。之后会进入优化器,将SQL查询语句的执行方案确定下来(优化器考虑查询成本,确定执行方案)。最后进行执行器,执行前查看用户是否有权限,有的话就会根据表的引擎定义,调用存储引擎API对表进行读写。
3. MySQL一条行记录是怎么存储的?
Compressed和Dynamic行格式和Compact非常类似,主要区别在于处理行溢出数据时有些区别。
COMPACT行格式
- 变长字段长度列表
varchar是变长的,在存储数据的时候,也要把数据占用的大小存起来,存到[变长字段长度列表]里面,读取数据的时候才能根据这个变长字段长度列表去读取相应长度的数据。其他的TEXT、BLOB等字段也是这么实现的。 [变长字段长度列表]只出现在数据表中有变长字段的时候。
- NULL值列表
表中的某些列可能会存储 NULL 值,如果把这些 NULL 值都放到记录的真实数据中会比较浪费空间,所以 Compact 行格式把这些值为 NULL 的列存储到 NULL值列表中。 如果存在允许 NULL 值的列,则每个列对应一个二进制位(bit),二进制位按照列的顺序逆序排列。
- 二进制位的值为1时,代表该列的值为NULL。
- 二进制位的值为0时,代表该列的值不为NULL。
另外,NULL 值列表必须用整数个字节的位表示(1字节8位),如果使用的二进制位个数不足整数个字节,则在字节的高位补 0。 当数据表的字段都定义成NOT NULL的时候,这时候表里的行格式就不会有NULL值列表了。
- 记录头信息
3.1 MySQL的NULL值是怎么存放的?
MySQL 的 Compact 行格式中会用「NULL值列表」来标记值为 NULL 的列,NULL 值并不会存储在行格式中的真实数据部分。 NULL值列表会占用 1 字节空间,当表中所有字段都定义成 NOT NULL,行格式中就不会有 NULL值列表,这样可节省 1 字节的空间。
3.2 MySQL 怎么知道 varchar(n) 实际占用数据的大小?
MySQL 的 Compact 行格式中会用「变长字段长度列表」存储变长字段实际占用的数据大小。
3.3 varchar(n) 中 n 最大取值为多少?
在MySQL中,VARCHAR(n) 中的 n 表示的是字符数(即最大字符数),而不是字节数。这意味着,如果在 VARCHAR(n) 中存储的是多字节字符集(例如UTF-8),则可以存储的字节数可能会大于 n,因为一个字符可能由多个字节组成。 例如,如果使用 VARCHAR(10) 来存储UTF-8编码的字符串,那么可以存储的最大字符数为10,但实际上可以存储的最大字节数可能会大于10。
要注意的是,如果使用的是单字节字符集(例如Latin1),则一个字符就是一个字节,因此 VARCHAR(n) 中的 n 表示的也就是最大字节数。
一行记录最大能存储 65535 字节的数据,但是这个是包含「变长字段字节数列表所占用的字节数」和「NULL值列表所占用的字节数」。所以, 我们在算 varchar(n) 中 n 最大值时,需要减去这两个列表所占用的字节数。 如果一张表只有一个 varchar(n) 字段,且允许为 NULL,字符集为 ascii。varchar(n) 中 n 最大取值为 65532。 计算公式:65535 - 变长字段字节数列表所占用的字节数 - NULL值列表所占用的字节数 = 65535 - 2 - 1 = 65532。 如果有多个字段的话,要保证所有字段的长度 + 变长字段字节数列表所占用的字节数 + NULL值列表所占用的字节数 <= 65535。
变长字段类型,需要再加2字节(用于存储字段实际数据的长度值),不管三七二十一都使用2字节表示可变字段的长度
3.4 行溢出后,MySQL 是怎么处理的?
如果一个数据页存不了一条记录,InnoDB 存储引擎会自动将溢出的数据存放到「溢出页」中。 Compact 行格式针对行溢出的处理是这样的:当发生行溢出时,在记录的真实数据处只会保存该列的一部分数据,而把剩余的数据放在「溢出页」中,然后真实数据处用 20 字节存储指向溢出页的地址,从而可以找到剩余数据所在的页。 Compressed 和 Dynamic 这两种格式采用完全的行溢出方式,记录的真实数据处不会存储该列的一部分数据,只存储 20 个字节的指针来指向溢出页。而实际的数据都存储在溢出页中。
Compact:部分数据+溢出页地址,把溢出数据保存在溢出页中
Compressed和Dynamic:溢出页地址,把溢出数据全部保存在溢出页中
二、索引篇
1. MySQL支持哪些存储引擎?默认使用哪个?
MySQL支持多种存储引擎,常见的有MyISAM、InnoDB、Memory。可以通过show engines()来查看Mysql支持的所有存储引擎以及默认的存储引擎。 MySQL 5.5.5 之前,MyISAM 是 MySQL 的默认存储引擎。5.5.5 版本之后,InnoDB 是 MySQL 的默认存储引擎。
2. MySQL存储引擎架构了解吗?
MySQL存储引擎采用的是插件式架构,支持多种存储引擎,我们甚至可以为不同的数据库表设置不同的存储引擎以适应不同场景的需要。存储引擎是基于表的,而不是数据库。 可以根据MySQL定义的存储引擎实现标准接口来编写一个属于自己的存储引擎,也可以使用官方提供的存储引擎。
3. 什么是索引?什么是存储引擎?
索引:是帮助存储引擎快速获取数据的一种数据结构,形象的说就是索引是数据的目录。(索引是以空间换时间的设计思想) 存储引擎:就是如何存储数据、如何为存储的数据建立索引和如何更新、查询数据等技术的实现方法。
4. 索引的分类
按[物理存储]分类
1. 主键索引的 B+Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的 B+Tree 的叶子节点里;
2. 二级索引的 B+Tree 的叶子节点存放的是主键值+二级索引的索引列的值,而不是实际数据。
按[字段特性]分类
1.主键索引:主键索引就是建立在主键字段上的索引,通常在创建表的时候一起创建,一张表最多只有一个主键索引,索引列的值不允许有空值。
2. 唯一索引:唯一索引建立在 UNIQUE 字段上的索引,一张表可以有多个唯一索引,索引列的值必须唯一,但是允许有空值。
3. 普通索引:普通索引就是建立在普通字段上的索引,既不要求字段为主键,也不要求字段为 UNIQUE。
4. 前缀索引:前缀索引是指对字符类型字段的前几个字符建立的索引,而不是在整个字段上建立的索引,前缀索引可以建立在字段类型为 char、 varchar、binary、varbinary 的列上。使用前缀索引的目的是为了减少索引占用的存储空间,提升查询效率。
按[字段个数]分类
单列索引:建立在单列上的索引称为单列索引,比如主键索引
联合索引:建立在多列上的索引成为联合索引(通过将多个字段组合成一个索引)
因此,使用联合索引时,存在最左匹配原则,也就是按照最左优先的方式进行索引的匹配。在使用联合索引进行查询的时候,如果不遵守[最左匹配原则],联合索引会失效,这样就无法利用到索引快速查询的特性了。
联合索引的最左匹配原则,在遇到范围查询(如 >、<)的时候,就会停止匹配,也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段无法用到联合索引。注意,对于 >=、<=、BETWEEN、like 前缀匹配的范围查询,并不会停止匹配。
建立联合索引时,要把区分度大的字段排在前面,这样区分度大的字段越有可能被更多的SQL使用到
- 比如性别的区分度就很小,不适合建立索引或不适合排在联合索引靠前的位置,而UUID这类字段就比较适合做索引或排在联合索引列的靠前的位置
MySQL有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比(惯用的百分比界限时30%)很高的适合,它一般就会忽略索引,进行全表扫描
B+Tree是一种多叉树,叶子节点才存放数据,非叶子节点只存放索引,而且每个节点里的数据是按主键顺序存放的,每一层父节点的索引值都会出现在下层子节点的索引值中,因此在叶子节点中,包括了所有的索引值信息,并且每一个叶子节点都会有两个指针,分别指向下一个叶子节点和上一个叶子节点,形成一个双向链表。 B+Tree相比于B树和二叉树来说,最大的优势在于查询效率很高,因为即使在数据量很大的情况下,查询一个数据的磁盘IO依然维持在3-4次。 先会检查二级索引中的B+Tree的索引值,找到对应的叶子节点,然后获取主键值,然后再通过主键索引中的B+Tree树查找到对应的叶子节点,然后获取整行数据。这个过程叫做【回表】,也就是说要查找两个B+Tree才能找到数据 在二级索引的B+Tree就能查询到结果的过程就叫做【索引覆盖】,也就是只需要查找一个B+Tree就能找到数据。
在查询时使用了二级索引,如果查询的数据能在二级索引里查询得到,那么就不需要进行回表操作,这个过程就是覆盖索引。
5. 为什么MySQL InnoDB选择B+Tree作为索引的数据结构?
- B+ 树的非叶子节点不存放实际的记录数据,仅存放索引,因此数据量相同的情况下,相比存储即存索引又存记录的 B 树,B+树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O次数会更少。
- B+ 树有大量的冗余节点(所有非叶子节点都是冗余索引),这些冗余索引让 B+ 树在插入、删除的效率都更高,比如删除根节点的时候,不会像 B 树那样会发生复杂的树的变化;
- B+ 树叶子节点之间用链表连接了起来,有利于范围查询,而 B 树要实现范围查询,因此只能通过树的遍历来完成范围查询,这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树。
1. B树因为非叶子节点也存储数据,所以相比B+树存储的数据比较少
2. 二叉树的每个父节点的儿子节点个数只能是2个,这就比B+Tree高出不少,检索到目标数据所经历的IO次数较多
3. Hash适合做等值查询,不适合做范围查询,这也是B+Tree索引要比Hash表索引有着更为广泛的使用场景的原因
6. 什么时候需要/不需要创建索引?
索引最大的好处是提高查询速度,但是索引也有缺点,比如:
- 需要占用物理空间,数据越大,占用空间越大
- 创建索引和维护索引需要耗费时间,这种时间随着数据量的增加而增大
- 会降低表增删改的效率,因为每次增删改索引,B+树为了维护索引的有序性,都需要进行动态维护
什么时候使用索引?
- 字段的数值具有唯一性
- 经常用于WHERE查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引
- 经常用于GROUP BY和ORDER BY的字段,这样在查询的时候就不需要再去做一次排序了,因为我们已经知道了建立索引之后在B+Tree中的记录都是排好序的。
MySQL会在包含GROUP BY子句的查询中默认添加上ORDER BY子句。
什么时候不需要使用索引?
- WHERE条件、GROUP BY、ORDER BY里用不到的字段
索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的,因为索引是会占用物理空间的
- 字段中存在大量重复数据,不需要创建索引
索引的价值是帮助快速定位。如果想要定位的数据有很多,那么索引就失去了它的实用价值。当数据重复度大时,比如高于10%的时候,也不需要对这个字段建立索引。
- 表数据太少的时候,不需要创建索引
表记录太少,是否创建索引对查询效率的影响并不大。甚至说,查询花费的时间可能比遍历索引的时间还要短,索引可能不会产生优化效果。
- 经常更新的字段不用创建索引
因为索引字段频繁修改,由于要维护 B+Tree的有序性,那么就需要频繁的重建索引,这个过程是会影响数据库性能的。
- 避免对经常更新的表创建过多的索引
- 不建议用无序的值作为索引
例如身份证、UUID(在索引比较时需要转为 ASCII,并且插入时可能造成页分裂)、MD5、HASH、无序长字符串等。
- 删除不再使用或者很少使用的索引
表中的数据被大量更新,或者数据的使用方式被改变后,原有的一些索引可能不再需要。数据库管理员应当定期找出这些索引,将他们删除,从而减少索引对更新操作的影响。
- 不要定义冗余或重复索引
有时候有意或者无意的就对同一个列创建了多个索引,比如:index(a,b,c)相当于index(a)、index(a,b)、index(a、b、c)
7. 有什么索引优化的方法?
下面列举一下几种常见优化索引的方法:
- 前缀索引优化
- 覆盖索引优化
- 主键索引最好是自增的
- 防止索引失效
前缀索引优化
前缀索引顾名思义就是使用某个字段中字符串的前几个字符建立索引。 使用前缀索引是为了减少索引字段大小,可以增加一个索引页中存储的索引值数量,有效提高索引的查询速度。 不过,前缀索引有一定的局限性:
- order by无法使用前缀索引
- 无法把前缀引用作覆盖索引
索引覆盖优化
主键索引最好是自增的
InnoDB创建主键索引默认为聚簇索引,数据被存放在B+Tree的叶子节点上。也就是说,同一个叶子节点内的各个数据是按照主键顺序存放的,因此,每当有一条新的数据插入时,数据库会根据主键将其插入到对应的叶子节点中。 另外,主键字段的长度不要太大,因为主键字段长度越小,意味着二级索引的叶子节点越小(二级索引的叶子节点存放的数据是主键值和二级索引列值),这样二级索引占用的空间也就越小。
索引最好设置为NOT NULL
防止索引失效
简单列举几条,发生索引失效的情况:
- 当我们使用左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx%这两种方式都会造成索引失效;
- 当我们在查询条件中对索引列做了计算、函数、类型转换操作,这些情况下都会造成索引失效;
- 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效。
- 在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。
总结
8. 从数据页的角度看B+树
InnoDB是如何存储数据的?
MySQL支持多种存储引擎,不同的存储引擎,存储数据的方式是不同的,我们最常使用的是InnoDB 存储引擎。 记录是按照行来存储的,但是数据库的读取并不是以行为单位,否则一次读取(也就是一次I/O操作)只能处理一行数据,效率非常低。 因此,InnoDB的数据是按[数据页]为单位来读写的。也就是说,当需要读取一条记录的时候,并不是将这个记录本身从磁盘读出来,而是以页为单位,将其整体读入内存。 数据库的 I/O 操作的最小单位是页,InnoDB 数据页的默认大小是 16KB,意味着数据库每次读写都是以 16KB 为单位的,一次最少从磁盘中读取 16K 的内容到内存中,一次最少把内存中的 16K 内容刷新到磁盘中。 在File Header中有两个指针,分别指向上一个数据页和下一个数据页,连接起来相当于一个双向链表。采用链表的结构是让数据页之间不需要是物理上的连续,而是逻辑上的连续。
B+树是如何进行查询的?
InnoDB里的B+数中的每个节点都是一个数据页,每个数据页都有对应的一个页目录,可以看到,在定位记录在哪一个页时,是通过二分法快速定位到包含该记录的页。定位到该页后,又会在该页内进行二分法快速定位记录所在的分组(槽号),最后在分组内进行遍历查找。
总结
InnoDB 的数据是按「数据页」为单位来读写的,默认数据页大小为 16 KB。每个数据页之间通过双向链表的形式组织起来,物理上不连续,但是逻辑上连续。 数据页内包含用户记录,每个记录之间用单向链表的方式组织起来,为了加快在数据页内高效查询记录,设计了一个页目录,页目录存储各个槽(分组),且主键值是有序的,于是可以通过二分查找的方式进行检索从而提高效率。
为了高效查询记录所在的数据页,InnoDB 采用 b+ 树作为索引,每个节点都是一个数据页。 如果叶子节点存储的是实际数据的就是聚簇索引,一个表只能有一个聚簇索引;如果叶子节点存储的不是实际数据,而是主键值+二级索引列值则就是二级索引,一个表中可以有多个二级索引。 在使用二级索引进行查找数据时,如果查询的数据能在二级索引找到,那么就是「索引覆盖」操作,如果查询的数据不在二级索引里,就需要先在二级索引找到主键值,需要去聚簇索引中获得数据行,这个过程就叫作「回表」。
9. 为什么MySQL InnoDB采用B+树作为索引?
怎样的索引的数据结构是好的?
MySQL的数据是持久化的,意味着数据(索引+记录)是保存在磁盘上的,因为这样即使设备断电了,数据也不会丢失。 内存的访问速度是纳秒级别的,而磁盘访问的速度是毫秒级别的,也就是读取同样大小的数据,磁盘中读取的速度比从内存中读取的速度要慢上万倍,甚至几十万倍。 由于数据库的索引是保存到磁盘上的,因此当我们通过索引查找某行数据的时候,就需要先从磁盘读取索引到内存,再通过索引从磁盘中找到某行数据,如此往复,然后将用户数据读入到内存,也就是说查询过程中会发生多次磁盘I/O,而磁盘I/O次数越多,所消耗的时间也就越大。
总结
MySQL 是会将数据持久化在硬盘,而存储功能是由 MySQL 存储引擎实现的,所以讨论 MySQL 使用哪种数据结构作为索引,实际上是在讨论存储引使用哪种数据结构作为索引,InnoDB 是 MySQL 默认的存储引擎,它就是采用了 B+ 树作为索引的数据结构。 要设计一个 MySQL 的索引数据结构,不仅仅考虑数据结构增删改的时间复杂度,更重要的是要考虑磁盘 I/0 的操作次数。因为索引和记录都是存放在硬盘,硬盘是一个非常慢的存储设备,我们在查询数据的时候,最好能在尽可能少的磁盘 I/0 的操作次数内完成。 二分查找树虽然是一个天然的二分结构,能很好的利用二分查找快速定位数据,但是它存在一种极端的情况,每当插入的元素都是树内最大的元素,就会导致二分查找树退化成一个链表,此时查询复杂度就会从 O(logn)降低为 O(n)。 为了解决二分查找树退化成链表的问题,就出现了自平衡二叉树,保证了查询操作的时间复杂度就会一直维持在 O(logn) 。但是它本质上还是一个二叉树,每个节点只能有 2 个子节点,随着元素的增多,树的高度会越来越高。 而树的高度决定于磁盘 I/O 操作的次数,因为树是存储在磁盘中的,访问每个节点,都对应一次磁盘 I/O 操作,也就是说树的高度就等于每次查询数据时磁盘 IO 操作的次数,所以树的高度越高,就会影响查询性能。 B 树和 B+ 都是通过多叉树的方式,会将树的高度变矮,所以这两个数据结构非常适合检索存于磁盘中的数据。 但是 MySQL 默认的存储引擎 InnoDB 采用的是 B+ 作为索引的数据结构,原因有:
- B+ 树的非叶子节点不存放实际的记录数据,仅存放索引,因此数据量相同的情况下,相比存储即存索引又存记录的 B 树,B+树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O次数会更少。
- B+ 树有大量的冗余节点(所有非叶子节点都是冗余索引),这些冗余索引让 B+ 树在插入、删除的效率都更高,比如删除根节点的时候,不会像 B 树那样会发生复杂的树的变化;
- B+ 树叶子节点之间用链表连接了起来,有利于范围查询,而 B 树要实现范围查询,因此只能通过树的遍历来完成范围查询,这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树;
- B+树比B树磁盘I/O操作次数稳定一些;
B树是一种多路平衡搜索树,在存储大量数据的时候,它的整体高度相比二叉树来说会矮很多,对于数据库来说,所有的数据必然是存储在磁盘上的,而磁盘IO的效率很低,特别是在随机磁盘IO的情况下,效率更低,所以树的高度就会决定磁盘IO的一个次数,树的高度越低,磁盘IO次数越少,对性能提升越大,这就是为什么采用B树作为存储结构的原因,MySql使用B+树(说B+树与B树的区别)
1. B+树非叶子几点不存储数据,所以每一层能够存储的索引会增加,意味着B+树在层高相同的情况下存储的数据量比B树多,使得IO次数更少
2. MySQL范围查询,B树需要遍历所有节点
3. B+树IO次数比B树稳定
4. B+树全局扫描能力强一些,B+树需要遍历整个树
10. 索引失效有哪些?
虽然InnoDB和MyISAM都支持B+树索引,但是它们数据的存储结构实现方式不同。不同之处在于:
- InnoDB存储引擎:B+树索引的叶子节点保存数据本身
- MyISAM存储引擎:B+树索引的叶子节点保存数据的物理地址
对索引使用左或者左右模糊匹配
当我们使用左或者左右模糊匹配的时候,也就是like %xx或者like %xx%这两种方式都会造成索引失效。
如果使用name like '%林'方式来查询,因为查询的结果可能是[陈林、张林、周林]等之类的,所以不知道从哪个索引值开始比较,于是就只能通过全表扫描的方式来查询。
对索引使用函数
如果查询条件中对索引字段使用函数,就会导致索引失效。 为什么索引会失效呢? 因为索引保存的是索引字段的原始值,而不是经过函数计算后的值,自然就没办法走索引了。 不过,从MySQL8.0开始,索引特性增加了函数索引,即可以针对函数计算后的值建立一个索引,也就是说该索引的值是函数计算后的值,所以就可以通过扫描索引来查询数据。
对索引进行表达式计算
在查询条件中对索引进行表达式计算,也是无法走索引的。 为什么索引会失效呢?
SELECT * FROM t_user WHERE id + 1 = 10; // 索引失效
SELECT * FROM t_user WHERE id = 10 - 1; // 索引不失效
因为索引保存的是索引字段的原始值,而不是id+1表达式计算后的值,所以无法走索引,只能通过把索引字段的取值都取出来,然后依次进行表达式的计算来进行条件判断,因此采用的就是全表扫描的方式。
对索引隐式类型转换
如果索引字段是字符串类型,但是在查询条件中,输入的参数是整型的话,执行计划的结果发现这条语句会走全表扫描。 MySQL在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后进行比较。
SELECT * FROM t_user WHERE phone = 130000001; // 索引失效
SELECT * FROM t_user WHERE id = "1"; // 索引不会失效
联合索引非最左匹配
创建联合索引时,我们需要注意创建时的顺序问题,因为联合索引(a,b,c)和(c,b,a)在使用的时候会存在差别。 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配。
索引下推(tui)的大概原理是:截断的字段不会在Server层进行条件判断,而是会被下推到[存储引擎层]进行条件判断(因为c字段的值是在(a,b,c)联合索引里的),然后过滤出符合条件的数据后再返回给Server层。由于在存储引擎层就过滤掉大量数据,无需再回表读取数据来进行判断,减少回表次数,从而提升了性能。
索引下推(Index Condition Pushdown):它能减少回表次数,提高查询效率。"下推"是将部分服务层(上层)的事情,交给下层(存储引擎层)处理。
在没有开启索引下推之前,MySQL的查询方式是这样的:
- 先从二级索引中根据name匹配到姓张的人的所有数据行,得到主键索引id分别是1和4
- 分别用1和4去聚簇索引中找到匹配的数据行
- 然后在MySQL的Server层再对数据使用'age = 18'这个条件进行过滤
这种方式我们会发现会涉及两次回表操作。索引下推就是针对这个场景的优化。
索引下推就是把过滤的场景下推给存储引擎层。 优化之后:当根据name和age进行查询的时候,直接在存储引擎中根据name和age进行过滤,得到匹配后的数据行,再回表查询(只涉及一次回表查询)
为什么会失效? 原因是,在联合索引的情况下,数据是按照索引第一列排序,第一列数据相同时才会按照第二列排序。 也就是说,如果我们想使用联合索引中尽可能多的列,查询条件中各个列必须是联合索引中从最左边开始连续的列。如果我们仅仅按照第二列搜索,肯定无法走索引。
WHERE子句中的OR
在WHERE子句中,如果在OR前的条件是索引列,而在OR后的条件列不是索引列,那么索引会失效。 这是因为OR的含义就是两个只要满足一个即可,因此只有一个条件列是索引列是没有意义的,只要有条件列不是索引列,就会走全表扫描。
总结
今天给大家介绍了 6 种会发生索引失效的情况:
- 当我们使用左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx%这两种方式都会造成索引失效;
- 当我们在查询条件中对索引列使用函数,就会导致索引失效。
- 当我们在查询条件中对索引列进行表达式计算,也是无法走索引的。
- MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。如果字符串是索引列,而条件语句中的输入参数是数字的话,那么索引列会发生隐式类型转换,由于隐式类型转换是通过 CAST 函数实现的,等同于对索引列使用了函数,所以就会导致索引失效。
- 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效。
- 在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。
11. MySQL使用like "%x",索引一定会失效吗?
从这个思考题我们知道了,使用左模糊匹配(like "%xx")并不一定会走全表扫描,关键还是看数据表中的字段。 如果数据库表中的字段只有主键+二级索引,那么即使使用了左模糊匹配,也不会走全表扫描(type=all),而是走全扫描二级索引树(type=index)。
12. count(*)和count(1)有什么区别?哪个性能最好?
哪种count性能最好?
count()是什么?
count()是一个聚合函数,函数的参数不仅可以是字段名,也可以是其他任意表达式,该函数的作用是统计符合查询条件的记录中,函数指定的参数不为NULL的记录有多少个。
COUNT(查询条件)
SELECT COUNT(name) FROM t_user; 从t_user表中,统计name字段不会NULL的记录有多少个 SELECT COUNT() FROM t_user; 从t_user表中,字段不为NULL的记录有多少个,由于*永远不为NULL,所以实际上是统计表中有多少个字段.
count(主键字段)执行过程是怎样的?
在通过count函数统计有多少个记录时,MySQL的server层会维护一个名叫count的变量。 server层会循环向InnoDB读取一条记录,如果count函数指定的参数不为NULL,那么会将变量count加1,直到符合查询的全部记录被读取完,就退出循环。最后将count变量的值发送给客户端。
- 如果表里只有主键索引,没有二级索引时,那么,InnoDB 循环遍历聚簇索引,将读取到的记录返回给 server 层,然后读取记录中的 id 值,就会 id 值判断是否为 NULL,如果不为 NULL,就将 count 变量加 1。
- 如果表里有二级索引时,InnoDB 循环遍历的对象就不是聚簇索引,而是二级索引。(这是因为相同数量的二级索引记录可以比聚簇索引记录占用更少的存储空间,所以二级索引树比聚簇索引树小,这样遍历二级索引的 I/O 成本比遍历聚簇索引的 I/O 成本小,因此「优化器」优先选择的是二级索引。)
count(1)执行过程是怎样的?
- 如果表里只有主键索引,没有二级索引时。那么,InnoDB 循环遍历聚簇索引(主键索引),将读取到的记录返回给 server 层,但是不会读取记录中的任何字段的值,因为count函数的参数是1,不是字段,所以不需要读取记录中的字段值。参数1很明显并不是 NULL,因此server层每从InnoDB读取到一条记录,就将 count 变量加1。
- 如果表里有二级索引时,InnoDB 循环遍历的对象就二级索引了。
count(*)执行过程是怎样的?
所以,count(*)执行过程跟count(1)执行过程基本一样的,性能没有什么差异。
count(字段)执行过程是怎样的?
count(字段)的执行效率相比前面count(1)、count(*)、count(主键字段)执行效率是最差的。
最坏的情况就是遍历全表
总结
count(1)、 count(*)、 count(主键字段)在执行的时候,如果表里存在二级索引,优化器就会选择二级索引进行扫描。 所以,如果要执行 count(1)、 count(*)、 count(主键字段) 时,尽量在数据表上建立二级索引,这样优化器会自动采用 key_len 最小的二级索引进行扫描,相比于扫描主键索引效率会高一些。 再来,就是不要使用 count(字段) 来统计记录个数,因为它的效率是最差的,会采用全表扫描的方式来统计。如果你非要统计表中该字段不为 NULL 的记录个数,建议给这个字段建立一个二级索引。
为什么要通过遍历的方式来计数?
如何优化count(*)?
第一种:近似值
如果业务对于统计个数不需要很精确的值,这时,我们就可以使用show table status或者explain命令对表进行估算。 执行explain命令效率是很高的,因为它并不会真正的去查询,下图中的rows字段就是expalin命令对表t_order记录的估算值。
第二种:额外表保存计数值
如果想精确的获取表的记录数,我们可以将这个计数值保存到单独的一张计数表中。 当我们在数据表中插入一条记录时,将计数表中的计数字段+1.也就是说,在新增和删除操作时,我们需要额外维护这个计数表。
三、事务篇
事务就是一组SQL操作,是一个原子操作单元。
1. 事务的隔离级别是怎么实现的?
事务有哪些特性?
- 原子性(Atomicity):一个事务的所有操作,要么都做,要么都不不做。
- 一致性(Consistency):是指事务操作前和操作后,数据前后都满足完整性约束,数据库保持一致性状态。(一致性是指事务执行前后,数据从一个合法性状态转变到另一个合法性状态,这种状态是语义上的,而不是语法上的,跟具体的业务有关)。
- 隔离性(Isolation):一个事务的执行不能被其他事务干扰。
- 持久性(Durablity):一个事务一旦被提交,它对数据库中数据的改变就是永久的,接下来的其他操作和数据库故障不对其有任何影响。
只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障,也就是A、I、D是手段,C是目的!
InnoDB引擎通过什么技术来保证事务的这四个特性的呢?
- 持久性是通过redo log(重做日志)来保证的
- 原子性是通过undo log(回滚日志)来保证的
- 隔离性是通过MVCC(多版本并发控制)或锁机制来保证的
- 一致性是通过持久性+原子性+隔离性来保证的
并发事务会引发什么问题?
MySQL服务端是允许多个客户端连接的,这意味着MySQL会出现同时处理多个事务的情况。 那么多个并发事务会出现脏读、不可重复读、幻读的问题。
脏读
如果一个事务[读到]了另一个[未提交事务修改过的数据],就意味着发生了[脏读]现象。
事务A读到了事务B未提交修改过的数据,则事务A发生了脏读现象。
不可重复读
在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了[不可重复读]现象。
事务A两次读取同一数据,出现前后两次读到的数据不一样的情况,就意味着事务A发生了不可重复读现象。
幻读
在一个事务内多次查询某个符合查询条件的[记录数量],如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了[幻读]现象。
事务A多次查询某个符合查询条件的记录数量,出现前后两次查询到的记录数量不一样的情况,就意味着事务A发生了幻读现象。
幻读强调的是一个事务按照某个相同条件多次读取记录时,后读取时读到了之前没有读到的记录。
脏读、不可重复读、幻读都是发生在读-写或写-读的情况下。
事务的隔离级别有哪些?
- 脏读:读到其他事务未提交的数据;
- 不可重复读:前后读取的数据不一致;
- 幻读:前后读取的记录数量不一致。
SQL 标准提出了四种隔离级别来规避这些现象,隔离级别越高,性能效率就越低,这四个隔离级别如下:
- 读未提交(read uncommitted),指一个事务还没提交时,它做的变更就能被其他事务看到;
- 读已提交(read committed),指一个事务提交之后,它做的变更才能被其他事务看到;
- 可重复读(repeatable read),指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别;
- 串行化(serializable ),会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;
这四种隔离界别具体是怎么实现的呢?
- 对于[读未提交]隔离级别的事务来说,因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了;
- 对于[串行化]隔离级别的事务来说,通过加读写锁的方式来避免并行访问;
- 对于[读已提交]和[可重复读]隔离级别的事务来说,它们是通过MVCC(
不单单只靠Read View) 来实现的,它们的区别在于创建 Read View 的时机不同,大家可以把 Read View 理解成一个数据快照,就像相机拍照那样,定格某一时刻的风景。「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View,而「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View。
MySQL 在「可重复读」隔离级别下,可以很大程度上避免幻读现象的发生(注意是很大程度避免,并不是彻底避免),所以 MySQL 并不会使用「串行化」隔离级别来避免幻读现象的发生,因为使用「串行化」隔离级别会影响性能。
Read View在MVCC里如何工作的?
我们需要了解两个知识:
- Read View中四个字段作用;
- 行记录中两个跟事务有关的隐藏列;
Read View是什么东西? Read View有四个重要的字段:
- m_ids:指的是在创建Read View时,当前数据库中[活跃事务]的事务id列表,注意是一个列表(或数组),"活跃事务"指的就是,启动了但还没提交的事务。
- min_trx_id:指的是在创建Read View时,当前数据库中[活跃事务]中事务id最小的事务,也就是m_ids的最小值。
- max_trx_id:这个并不是m_ids的最大值,而是创建Read View时当前数据库中应该给下一个事务的id值,也就是全局事务中最大的事务id值+1
- creator_trx_id:指的是创建该Read View的事务的事务id。
对于使用InnoDB存储引擎的数据库表,它的聚簇索引记录都包含下面两个隐藏列:
- trx_id:当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里;
- roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录。
Read View的规则
- 对于使用读未提交隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本机就好了
- 对于串行化隔离级别的事务来说,使用加锁的方式来访问记录
- 对于读已提交和可重复读隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就说假如另一个事务修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题是:需要判断下一个版本链中的哪个版本是当前事务可见的。
一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况:
如果记录的trx_id值小于Read View中的min_trx_id值,表示这个版本的记录是在创建Read View前已经提交的事务生成的,所以该版本的记录对当前事务可见。
如果记录的trx_id值大于等于Read View中的max_trx_id值,表示这个版本的记录是在创建Read View后才启动该的事务生成的,所以该版本的记录对当前事务不可见。
如果记录的trx_id值在Read View的min_trx_id和max_trx_id之间,需要判断trx_id是否在m_ids列表中:
- 如果记录的trx_id在m_ids列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。
- 如果记录的trx_id不在m_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。
这种通过[版本链]来控制并发事务访问同一个记录时的行为就叫做MVCC(多版本并发控制)。
MVCC的实现依赖于:隐藏字段、Undo Log(undo 日志)、Read View
对于读已提交和可重复读隔离级别来说,它是通过MVCC来实现的。 在MySQL中,READ COMMITTED和REPEATABLE READ隔离级别的一个非常大的区别是它们生成ReadView的时机不同。
MVCC整体操作流程
MVCC是通过数据行的多个版本管理来实现数据库的并发控制。
MVCC整体操作流程:
- 首先获取事务自己的版本号,也就是事务ID
- 获取(生成)ReadView
- 查询得到的数据,然后与ReadView中的事务版本号进行比较
- 如果不符合Read View规则(当前版本不能被访问),就需要从Undo Log中获取历史快照
- 最后返回符合规则的数据
背:所谓的MVCC(Multi-Version Concurrency Control,多版本并发控制)指的就是在使用READ COMMITTED、REPEATABLE READ这两种隔离级别在执行普通的SELECT(快照读)操作时访问记录的版本链的过程,这样可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。READ COMMITTED、REPEATABLE READ这两个隔离级别的一个很大不同就是:生成ReadView的时机不同,READ COMMITTED在每一次普通SELECT操作前都会生成一个ReadView,而REPEATABLE READ只在第一次普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。
MVCC机制实现了快照读,普通SELECT查询就是快照读,快照读到的数据有可能不是最新的数据,它主要是为了实现可重复度的事务隔离级别。 当前读的是最新版本的数据,当前读就是加了锁的增删改查语句。 在快照读的情况下,InnoDB通过MVCC机制解决了幻读现象; 在当前读的情况下,InnoDB是无法通过MVCC解决幻读的现象,因为它每次读取的都是最新的数据;
读已提交是如何工作的?
读已提交隔离级别是在每次读取数据时,都会生成一个新的Read View。
可重复读是如何工作的?
可重复读隔离级别是启动事务时生成一个Read View,然后整个事务期间都在用这个Read View。
总结
事务是在 MySQL 引擎层实现的,我们常见的 InnoDB 引擎是支持事务的,事务的四大特性是原子性、一致性、隔离性、持久性,我们这次主要讲的是隔离性。 当多个事务并发执行的时候,会引发脏读、不可重复读、幻读这些问题,那为了避免这些问题,SQL 提出了四种隔离级别,分别是读未提交、读已提交、可重复读、串行化,从左往右隔离级别顺序递增,隔离级别越高,意味着性能越差,InnoDB 引擎的默认隔离级别是可重复读。 要解决脏读现象,就要将隔离级别升级到读已提交以上的隔离级别,要解决不可重复读现象,就要将隔离级别升级到可重复读以上的隔离级别。 而对于幻读现象,不建议将隔离级别升级为串行化,因为这会导致数据库并发时性能很差。MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象,解决的方案有两种:
- 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
- 针对当前读(select ... for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select ... for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。
对于「读已提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同:
- 「读已提交」隔离级别是在每个 select 都会生成一个新的 Read View,也意味着,事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。
- 「可重复读」隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View,这样就保证了在事务期间读到的数据都是事务启动前的记录。
这两个隔离级别实现是通过「事务的 Read View 里的字段」和「记录中的两个隐藏列」的比对,来控制并发事务访问同一个记录时的行为,这就叫 MVCC(多版本并发控制)。(每个事务都有自己的Read View) 在可重复读隔离级别中,普通的 select 语句就是基于 MVCC 实现的快照读,也就是不会加锁的。而 select .. for update 语句就不是快照读了,而是当前读了,也就是每次读都是拿到最新版本的数据,但是它会对读到的记录加上 next-key lock 锁。
四、锁篇
- 采用MVCC方式的话,读(MVCC)-写(加锁)操作彼此并不冲突,性能更高 - 采用加锁方式的话,读-写操作彼此需要排队执行,影响性能
一般情况下,我们愿意采用MVCC来解决读-写操作并发执行的问题,但是业务在某些情况下,必须采用加锁的方式执行。
1. MySQL有哪些锁?
在MySQL的范围里,根据加锁的范围,可以分为全局锁、表锁和行锁三类。
全局锁
使用全局锁,整个数据库就处于只读状态了,这时其他线程执行以下操作,都会被阻塞:
- 对数据的增删改操作:比如insert、delete、update等语句;
- 对表结构的更改操作:比如alter table、drop table等语句。
// 添加全局锁 flush tables with read lock
// 释放全局锁
unlock tables
会话断开,全局锁会被自动释放。
全局锁的应用场景是什么? 全局锁的应用场景:做全库逻辑备份,这样在备份数据库期间,不会因为数据库或表结构的更新,而出现备份文件的数据与预期的不一样。
全局锁会带来什么缺点呢? 加上全局锁,意味着整个数据库都是只读状态。 那么如果数据库里有很多数据,备份就会花费很多的时间,关键是备份期间,业务只能读数据,而不能更新数据,这样会造成业务停滞。
既然备份数据库数据的时候,使用全局锁会影响业务,那么有什么其他方式可以避免?
表级锁
MySQL表级锁有哪些?具体怎么用的 MySQL里面的表级锁有这几种:
- 表锁
- 元数据锁(MDL)
- 意向锁
- AUTO-INC锁
表锁
// 表级别的共享锁,也就是读锁 lock tables t_student read;
// 表级别的独占锁,也就是写锁
lock tables t_student write;
// 释放表锁
unlock tables;
当会话退出后,也会释放所有表锁
元数据锁(MDL)
我们不需要显式的使用MDL,因为当我们对数据库表进行操作时,会自动给这个表加上MDL;
- 对一张表进行CRUD操作时,加的是MDL读锁;
- 对一张表做结构变更操作的时候,加的是MDL写锁;
MDL是为了保证用户对表执行CRUD操作时,防止其他线程对这个表的结构做了变更。 当有线程在执行select语句(加MDL读锁)的期间,如果有其他线程要更改表的结构(申请MDL写锁),那么会被阻塞,直达执行完select语句(释放MDL读锁) 当有线程对表结构进行变更(加MDL写锁)的期间,如果有其他线程执行了CRUD操作(申请MDL读锁),那么会被阻塞,直到表结构变更完成(释放MDL写锁)
MDL不需要显示调用,那它是在什么时候释放的? MDL是在事务提交后才会释放,这意味着事务执行期间,MDL是一直持有的。
所以为了能安全的对表结构进行变更,在对表结构进行变更前,先要看看数据库中的长事务,是否有事务已经对表加上了MDL读锁,如果可以考虑kill掉这个长事务,然后再做表结构的变更。
意向锁
- 在使用InnoDB引擎的表里对某些记录加上共享锁之前,需要在表级别上加上一个意向共享锁;
- 在使用InnoDB引擎的表里对某些记录加上独占锁之前,需要先在表级别上加上一个意向独占锁;
也就是在执行插入、更新、删除操作,需要先对表加上意向独占锁,然后对该记录加独占锁。
普通的select是不会加行级锁的,普通的select语句时利用MVCC实现一致性读,是无锁的 不过,select也是可以对记录加共享锁和独占锁的:
//先在表上加上意向共享锁,然后对读取的记录加共享锁 select ... lock in share mode;
//先表上加上意向独占锁,然后对读取的记录加独占锁
select ... for update;
意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁(lock tables ... read)和独占表锁(lock tables ... write)发生冲突。
表锁和行锁是满足读读共享、读写互斥、写写互斥的。 如果没有「意向锁」,那么加「独占表锁」时,就需要遍历表里所有记录,查看是否有记录存在独占锁,这样效率会很慢。 那么有了「意向锁」,由于在对记录加独占锁前,先会加上表级别的意向独占锁,那么在加「独占表锁」时,直接查该表是否有意向独占锁,如果有就意味着表里已经有记录被加了独占锁,这样就不用去遍历表里的记录。 所以,意向锁的目的是为了快速判断表里是否有记录被加锁。
AUTO-INC锁
表里的主键通常都会设置成自增的,这是通过对主键字段声明AUTO-INCREMENT属性实现的。 之后可以在插入数据时,可以不指定主键,数据库会自动给主键赋值递增的值,这主要是通过ANTO-INC锁实现的。 AUTO-INC锁是特殊的表锁,锁不再是一个事务提交后才释放,而是执行完插入语句后就会立即释放。 在插入数据时,会加一个表级别的 AUTO-INC 锁,然后为被 AUTO_INCREMENT 修饰的字段赋值递增的值,等插入语句执行完成后,才会把 AUTO-INC 锁释放掉。 那么,一个事务在持有 AUTO-INC 锁的过程中,其他事务的如果要向该表插入语句都会被阻塞,从而保证插入数据时,被 AUTO_INCREMENT 修饰的字段的值是连续递增的。
行级锁
InnoDB是支持行级锁的,而MyISAM引擎是不支持行级锁的; 普通的select语句是不会对记录加锁的,因为它属于快照读。 如果要在查询时对记录加行锁,可以使用下面这两个方式,这种查询会加锁的语句称为锁定读。
//先在表上加上意向共享锁,然后对读取的记录加共享锁 select ... lock in share mode;
//先表上加上意向独占锁,然后对读取的记录加独占锁
select ... for update;
行级锁主要有三种类型:
- Record Lock:记录锁,也就是仅仅把一条记录锁上
- Gap Lock:间隙锁,锁定一个范围,但是不包含记录本身
- Next-key Lock:Record Lock + Gap Lock的组合,锁定一个范围,并且锁定记录本身
Record Lock(记录锁)
Record Lock称为记录锁,锁住的是一条记录。而且记录锁是有S锁和X锁之分:
- 当一个事务对一条记录加了 S 型记录锁后,其他事务也可以继续对该记录加 S 型记录锁(S 型与 S 锁兼容),但是不可以对该记录加 X 型记录锁(S 型与 X 锁不兼容);
- 当一个事务对一条记录加了 X 型记录锁后,其他事务既不可以对该记录加 S 型记录锁(S 型与 X 锁不兼容),也不可以对该记录加 X 型记录锁(X 型与 X 锁不兼容)。
// 对主键id为1的这条记录加上X型的记录锁,这样其他事务就无法对这条记录进行修改
SELECT * FROM t_test WHERE id = 1 for update;
Gap Lock(间隙锁)
Gap Lock称为间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。 间隙锁虽然存在X型间隙锁和S型间隙锁,但是并没有什么区别,间隙锁之间是兼容的,即两个事务可以同时持有包含间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的就是为了防止插入幻影记录而提出的。
Next-Key Lock(临检锁)
Next-Key Lock称为临键锁,是Record Lock+Gap Lock的组合,锁定一个范围,并且锁定记录本身。 next-key lock是包含间隙锁+记录锁的,如果一个事务获取了X型的next-key lock,那么另外一个事务在获取相同范围的X型的next-key lock时,是会被阻塞的。
插入意向锁
一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁(next-key lock也包含间隙锁)。 如果有的话,插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻),在此期间会生成一个插入意向锁,表明事务想在某个区间插入新纪录,但是现在处于等待状态。
插入意向锁名字虽然有意向锁,但是它并不是意向锁,它是一种特殊的间隙锁,属于行级别锁。
2. MySQL是怎么加锁的?
下面介绍的是MySQL加行级锁的一些规则。
什么SQL语句会加行级锁? InnoDB引擎是支持行级锁的,而MyISAM引擎是不支持行级锁的。 普通的select语句是不会对记录加锁的(除了串行化隔离级别),因为它属于快照读,是通过MVCC实现的。 如果要在查询时对记录加行级锁,可以使用下面两个方式,这种查询会加锁的语句称为锁定读。
//对读取的记录加共享锁(S型锁) select ... lock in share mode;
//对读取的记录加独占锁(X型锁)
select ... for update;
除了上面这两条锁定读语句会加行级锁之外,update和delete操作都会加行级锁,且锁的类型都是独占锁(X型锁)。
//对操作的记录加独占锁(X型锁) update table .... where id = 1;
//对操作的记录加独占锁(X型锁)
delete from table where id = 1;
共享锁(S锁)满足读读共享,读写互斥。独占锁(X锁)满足写写互斥、读写互斥。
MySQL是怎么加行级锁的?
行级锁加锁规则比较复杂,不同的场景,加锁的形式是不同的。 加锁的对象是索引,加锁的基本单位是next-key lock,它是由记录锁和间隙锁组合而成的,next-key lock是前开后闭区间,而间隙锁是前开后开区间。 但是,在能使用记录锁或间隙锁就能避免幻读现象的场景下,next-key lock就会退化成记录锁或间隙锁。
唯一索引等值查询
用命令select * from performance_schema.data_locks\G;这条语句,查看事务执行SQL过程加了什么锁。
为什么唯一索引等值查询并且查询记录存在的场景下,该记录的索引中的 next-key lock 会退化成记录锁?
为什么唯一索引等值查询并且查询记录「不存在」的场景下,在索引树找到第一条大于该查询记录的记录后,要将该记录的索引中的 next-key lock 会退化成「间隙锁」?
唯一索引范围查询
当对唯一索引进行范围查询时,会对每一个扫描到的索引加next-key锁,然后如果遇到下面这些情况,会退化成记录锁或者间隙锁:
非唯一索引等值查询
非唯一索引范围查询
没有加索引的查询
3. update没加索引会锁全表?
InnoDB存储引擎的默认事务隔离级别是可重复读,但是在这个隔离级别下,在多个事务并发的时候,会出现幻读的问题,所谓的幻读是指在同一事务下,前后执行两次同样的查询语句,出现了结果集数量不同的情况。 因此InnoDB存储引擎实现了自己的行锁,通过next-key锁来锁住记录本身和记录之间的"间隙",防止其他事务在这个记录之间插入新的记录,从而避免了幻读现象。 在InnoDB事务中,对记录加锁的基本单位是next-key锁,但是会因为一些条件退化成间隙锁,或者记录锁。加锁的位置准确的说,锁是加在索引上而非行上。
当在数据量非常大的数据库表执行 update 语句时,如果没有使用索引,就会给全表的加上 next-key 锁, 那么锁就会持续很长一段时间,直到事务结束,而这期间除了 select ... from语句,其他语句都会被锁住不能执行,业务会因此停滞,接下来等着你的,就是老板的挨骂。 那 update 语句的 where 带上索引就能避免全表记录加锁了吗? 并不是。 关键还得看这条语句在执行过程种,优化器最终选择的是索引扫描,还是全表扫描,如果走了全表扫描,就会对全表的记录加锁了。
4. MySQL记录锁+间隙锁可以防止删除操作而导致的幻读吗?
MySQL是怎么解决幻读的?
MySQL记录锁+间隙锁可以防止删除操作而导致的幻读
MySQL的记录锁+间隙锁可以防止删除操作而导致的幻读问题。 接下来,来验证「 MySQL 记录锁+间隙锁可以防止删除操作而导致的幻读问题」的结论。 实验环境:MySQL 8.0 版本,可重复读隔离级。 现在有一张用户表(t_user),表里只有一个主键索引,表里有以下行数据: 现在有一个 A 事务执行了一条查询语句,查询到年龄大于 20 岁的用户共有 6 条行记录。 然后, B 事务执行了一条删除 id = 2 的语句: 此时,B 事务的删除语句就陷入了等待状态,说明是无法进行删除的。 因此,MySQL 记录锁+间隙锁可以防止删除操作而导致的幻读问题。
分析
事务A在主键索引上加了10个next-key锁,如下:
- X 型的 next-key 锁,范围:(-∞, 1]
- X 型的 next-key 锁,范围:(1, 2]
- X 型的 next-key 锁,范围:(2, 3]
- X 型的 next-key 锁,范围:(3, 4]
- X 型的 next-key 锁,范围:(4, 5]
- X 型的 next-key 锁,范围:(5, 6]
- X 型的 next-key 锁,范围:(6, 7]
- X 型的 next-key 锁,范围:(7, 8]
- X 型的 next-key 锁,范围:(8, 9]
- X 型的 next-key 锁,范围:(9, +∞]
这相当于把整个表都锁住了,其他事务进行增、删、改操作的时候都会被阻塞。 只有在事务A提交了事务,事务A执行过程中产生的锁才会被释放。
参考MySQL是如何加行锁的:非唯一索引等值查询和非唯一索引范围查询。
5. MySQL死锁了,怎么办?
- 记录锁:考虑X型与S型的关系
- 间隙锁:不区分X型与S型的关系,均兼容
- 插入意向锁:插入意向锁与间隙锁是冲突的,所以当其它事务持有该间隙锁的间隙时,需要等待其他事务释放间隙锁之后,才能获取到插入意向锁
尽管「插入意向锁」也属于间隙锁,但两个事务却不能在同一时间内,一个拥有间隙锁,另一个拥有该间隙区间内的插入意向锁(当然,插入意向锁如果不在间隙锁区间内则是可以的)。
Insert语句是怎么样加行级锁的?
’Insert语句在正常执行时是不会生成锁结构的,它是靠聚簇索引记录自带的trx_id隐藏列来作为隐式锁来保护记录的。 什么是隐式锁? 当事务需要加锁的时,如果这个锁不可能发生冲突,InnoDB会跳过加锁环节,这种机制称为隐式锁。隐式锁是 InnoDB 实现的一种延迟加锁机制,其特点是只有在可能发生冲突时才加锁,从而减少了锁的数量,提高了系统整体性能。 隐式锁就是在 Insert 过程中不加锁,只有在特殊情况下,才会将隐式锁转换为显示锁,这里我们列举两个场景。
- 如果记录之间加有间隙锁,为了避免幻读,此时是不能插入记录的;
- 如果 Insert 的记录和已有记录存在唯一键冲突,此时也不能插入记录;
案例
- Time1:事务A在主键索引上加入间隙锁,锁的范围(20,30)
- Time2:事务B在主键索引上加入间隙锁,锁的范围(20,30),这个锁与事务A的间隙锁虽然范围一样,但是两者相互兼容
- Time3:由于是插入语句,且事务B占有该范围的间隙锁,所以事务A会生成一个插入意向锁,并等待
- Time4:由于是插入语句,且事务A占有该范围的间隙锁,所以事务B会生成一个插入意向锁,并等待
事务 A 和事务 B 在执行完后 update 语句后都持有范围为(20, 30)的间隙锁,而接下来的插入操作为了获取到插入意向锁,都在等待对方事务的间隙锁释放,于是就造成了循环等待,满足了死锁的四个条件:互斥、占有且等待、不可强占用、循环等待,因此发生了死锁。
如何避免死锁?
五、日志篇
MySQL日志:undo log、redo log、bin log 有什么用?
- undo log(回滚日志):是InnoDB存储引擎生成的日志(存储引擎层),实现了事务中的原子性,主要用于事务回滚和MVCC。
- redo log(重做日志):是InnoDB存储引擎层生成的日志(存储引擎层),实现了事务中的持久性,主要用于掉电等故障恢复。
- bin log(二进制日志、归档日志):是(Server层)生成的日志,主要用于数据库备份和主从复制。
为什么需要undo log?
我们在执行一条"增删改"语句的时候,虽然没有输入begin开启事务和commit提交事务,但是MySQL会隐式开启事务来执行"增删改"语句,执行完就自动提交事务的,这样就保证了执行完"增删改"语句后,我们可以及时看到"增删改"的结果。
执行一条语句是否自动提交事务,是由 autocommit 参数决定的,默认是开启。所以,执行一条 update 语句也是会使用事务的。
一个事务在执行过程中,在还没有提交事务之前,如果MySQL发生了崩溃,要怎么回滚到事务之前的数据呢? 如果我们每次在事务执行过程中,都记录下回滚时需要的信息到一个日志里,那么在事务执行中发生了MySQL崩溃后,就不用担心无法回滚到事务之前的数据,我们可以通过undo log回滚到事务之前的数据。 实现这一机制就是undo log(回滚日志),它保证了事务的ACID特性中的原子性。 undo log是一种用于撤销回退的日志。在事务没提交之前,MySQL会先记录更新前的数据到undo log日志文件里面,当事务回滚时,可以利用undo log来进行回滚。 每当InnoDB引擎对一条记录进行操作(修改、删除、新增)时,要把回滚时需要的信息都记录到undo log里,比如:
- 在插入一条记录时,要把这条记录的主键值记录下来,这样之后回滚时只需要把这个主键值对应的记录删除掉;(插入—>删除)
- 在删除一条记录时,要把这条记录的内容全都记下来,这样之后回滚时再把这些内容组成的记录插入到表中;(删除—>插入)
- 在更新一条记录时,要把被更新的列的旧值记下来,这样之后回滚时再把这些列更新为旧值;(更新—>更新为旧值)
在发生回滚时,就读取undo log里的数据,然后做原先相反操作。 不同操作,需要记录的内容是不同的,所以不同类型的操作(修改、删除、新增)产生的undo log的格式也是不同的。
一条记录的每一次更新操作产生的undo log格式都有一个roll_pointer指针和一个trx_id事务id:
- 通过trx_id可以知道该记录时被哪个事务修改的;
- 通过roll_pointer指针可以将这些undo log串成一个链表,这个链表就叫做版本链;
另外,undo log还有一个作用,通过行记录隐藏字段 + Read View + Undo Log实现MVCC(多版本并发控制)来实现[读已提交]和[可重复读]隔离级别
因此,undo log两大作用:
- 实现事务回滚,保障事务的原子性。事务处理过程中,如果出现了错误或者用户执行了ROLLBACK语句,MySQL可以利用undo log中的历史数据将数据恢复到事务开始之前的状态。
- 实现MVCC(多版本并发控制)关键因素之一。MVCC是通过ReadView + undo log实现的。undo log为每一条记录保存多份历史数据,MySQL在执行快照读(普通select语句)的时候,会根据事务的Read View里的信息,顺着undo log的版本链找到满足可见性的记录。
为什么需要Buffer Pool?
InnoDB设计了一个缓冲池(Buffer Pool),来提高数据库的读写性能。 Buffer Pool本质上是InnoDB向操作系统申请的一段连续的内存空间,用来减少磁盘的IO操作,提高数据库的读写性能。
Buffer Pool缓存什么?
Undo页是记录什么? 答:开启事务后,InnoDB层更新记录前,首先要记录相应的undo log,如果是更新操作,需要把被更新前的旧值记录下来,也就是生成一条undo log,undo log会写入Buffer Pool中的Undo页面。
为什么需要redo log?
Buffer Pool是提高了读写效率,但是Buffer Pool是基于内存的,而内存总是不可靠的,万一断电重启,还没来得及落盘的脏页数据就会丢失。 为了防止断电导致数据丢失问题,InnoDB引擎就会先更新内存(同时标记为脏页),然后将本次对这个页的修改以redo log的形式记录下来,这个时候更新就算完成了。后续,InnoDB引擎会在适当的时候,由后台线程将缓存在Buffer Pool的脏页刷新到磁盘里,这里就是WAL技术(指MySQL的写操作并不是立即写到磁盘上,而是先写日志,然后在合适的时间再写到磁盘上)
什么是redo log?
redo log是物理日志,记录了某个数据页做了什么修改(比如对XXX表空间中的YYY数据页ZZZ偏移量的地方做了AAA更新),每当执行一个事务就会产生这样的一条或者多条物理日志。 在事务提交时,只要将redo log持久化到磁盘即可,可以不需要等到将缓存在Buffer Pool里的脏页数据持久化到磁盘。 当系统崩溃时,虽然脏页数据没有持久化,但是redo log已经持久化,接着MySQL重启后,可以根据redo log的内容,将所有数据恢复到最新的状态。
被修改Undo页面,需要记录对应redo log吗?
redo log和undo log区别在哪?
这两种日志属于InnoDB存储引擎的日志,主要区别:
- redo log记录了此次事务完成后的数据状态,记录的是更新之后的值;
- undo log记录了此次事务开始前的数据状态,记录的是更新之前的值;
事务提交之前发生了崩溃,重启后会通过undo log回滚事务,事务提交之后发生了崩溃,重启后会通过redo log恢复事务。 所以有了redo log,再通过WAL技术,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe(崩溃恢复)。可以看出来,redo log保证了事务四大特征中的持久性。
为什么需要redo log?
写入 redo log 的方式使用了追加操作, 所以磁盘操作是顺序写,而写入数据需要先找到写入位置,然后才写到磁盘,所以磁盘操作是随机写。 针对「顺序写」为什么比「随机写」更快这个问题,可以比喻为你有一个本子,按照顺序一页一页写肯定比写一个字都要找到对应页写快得多。 可以说这是 WAL 技术的另外一个优点:MySQL 的写操作从磁盘的「随机写」变成了「顺序写」,提升语句的执行性能。这是因为 MySQL 的写操作并不是立刻更新到磁盘上,而是先记录在日志上,然后在合适的时间再更新到磁盘上 。 至此, 针对为什么需要 redo log 这个问题我们有两个答案:
- 实现事务的持久性,让 MySQL 有 crash-safe 的能力,能够保证 MySQL 在任何时间段突然崩溃,重启后之前已提交的记录都不会丢失;
- 将写操作从「随机写」变成了「顺序写」,提升 MySQL 写入磁盘的性能。
redo log什么时候刷盘?
实际上,执行一个事务的过程,产生的的redo log也不是直接写入磁盘的,因为这样会产生大量的IO操作,而磁盘的运行速度远远慢于内存。 所以,redo log也有自己的缓存——redo log buffer,每当产生一条redo log时,会先写到redo log buffer,后续再持久化到磁盘。 主要有下面几个时机:
- MySQL正常关闭时;
- 当redo log buffer中记录的写入量大于redo log buffer内存空间的一半时,会触发落盘;
- InnoDB的后台线程每隔1秒,将redo log buffer持久化到磁盘
- 每次事务提交时都将缓存在redo log buffer里的redo log直接持久化到磁盘
redo log文件写满了怎么办?
默认情况下,InnoDB存储引擎有1个重做日志文件组,它由两个redo log文件组成,这两个redo日志的文件名叫:ib_logfile0和ib_logfile1。 redo log是为了防止Buffer Pool中的脏页丢失而设计的(实现事务的持久性)。那么如果随着系统运行,Buffer Pool 的脏页刷新到了磁盘中,那么 redo log 对应的记录也就没用了,这时候我们擦除这些旧记录,以腾出空间记录新的更新操作。
为什么需要binlog?
binlog(二进制日志):记录所有对MySQL数据库的修改操作,包括插入、更新和删除等。binlog主要用于数据恢复到指定时间点或者指定事务。 MySQL在完成一条更新操作后,Server层还会生成一条binlog,等之后事务提交的时候,会将该事务执行过程中产生的所有binlog统一写入binlog文件。 binlog文件是记录了所有数据库表结构变更和表数据修改的日志,不会记录查询类的操作,比如SELECT和SHOW操作。
redo log和binlog有什么区别?
如果不小心整个数据库的数据被删除了,能使用redo log文件恢复数据吗?
主从复制是怎么实现的?
MySQL的主从复制依赖于binlog,binlog记录了MySQL上的所有变化并以二进制形式保存在磁盘上。 主从复制的过程就是将binlog中的数据从主库传输到从库上。、 这个过程一般是异步的,也就是主库上执行事务操作的线程不会等待复制binlog的线程同步完成。
从库是不是越多越好? 不是的,随着从库数量的增加,从库连接上的IO线程也比较多,主库也要创建同样多的log dump线程来处理复制的请求,对主库资源消耗比较高,同时还受限于主库的网络带宽。 所以在实际使用中,一个主库一般跟 2~3 个从库(1 套数据库,1 主 2 从 1 备主),这就是一主多从的 MySQL 集群结构。
🎯binlog
binlog的概念
binlog是mysql的二进制日志。binlog只记录变更相关的操作信息,如语句的执行时间、时长、操作数据等额外信息。不包括select、show操作。
- 查看mysql的变更
- mysql数据备份与恢复
- mysql的主从复制
binlog二进制的格式
- statement模式
概述:记录数据库执行的原生sql语句
优点:不需要记录每一行的变化,日志量相对较小,节省IO,提高性能,主从复制网络带宽小
缺点:由于记录的是SQL执行语句,为了保证这些语句能在slave端执行,必须记录上下文信息来保证slave上执行能得到与master相同的结果。某些sql函数无法使用,比如sysdate(),会出现主从数据不一致的情况。
- row模式
概述:记录一行数据在更改前和更改后的变化
优点:详细记录了数据的修改情况,主从复制模式下可靠性高
缺点:row模式下二进制文件最大,占用硬盘空间,网络带宽高,对性能有一定的影响
- mixed模式
概述:statement模式与row模式的结合,更加sql判断使用哪种模式,默认采用statement模式,特殊情况会转换为row模式
使用了类似uuid()、user()、current_user()等不确定的函数
使用了UDF
使用了临时表
使用了insert delay函数
🎯为什么需要两阶段提交?
事务提交后,redo log和bin log都要持久化到磁盘,但是这两个是独立的逻辑,可能出现半成功的状态,这样就造成两份日志之间的逻辑不一致的情况。 可以看到,在持久化redo log和bin log这两份日志的时候,如果出现半成功的状态,就会造成主从环境的数据不一致。这是因为redo log影响主库的数据,bin log影响从库的数据,而这两个是独立的逻辑,所以redo log和bin log必须保持一致才能保证主从数据一致。 MySQL为了避免出现两份日志之间的逻辑不一致的问题,使用了两阶段提交来解决。两阶段提交其实是分布式事务一致性协议,它可以保证多个逻辑操作要不全部成功,要不全部失败,不会出现半成功的状态。 两阶段提交是把单个事务的提交拆分成了2个阶段,分别是【准备(Prepare)阶段】和【提交(Commit)阶段】,每个阶段都由协调者和参与者共同完成。
两阶段提交的过程是怎样的?
留
异常重启会出现什么现象?
留
MySQL磁盘I/O很高,有什么优化的方法?
留
总结
留
🎯日志系统:一条SQL更新语句是如何执行的?
在执行更新语句前要先连接数据库,这是连接器的工作。 接下来,分析器会通过词法分析和语法解析知道这是一条更新语句。优化器决定要使用ID索引,然后,执行器负责具体执行,找到这一行,然后更新。 与查询流程不一样的是,更新流程还涉及两个重要的日志模块,它就是今天要讨论的redo log(重做日志)和binlog(归档日志)。如果接触MySQL,那这两个词肯定是绕不过的。
重要的日志模块:redo log
如果MySQL每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,那么整个IO成本、查找成本都很高,为了解决这个问题,MySQL使用WAL技术,WAL(Write-Ahead Logging),它的关键点就是先写日志,再写磁盘。 具体来说,当有一条记录需要更新的时候,InnoDB引擎就会把记录写到redo log里面,并更新内存,这个时候更新就算完成了。同时,InnoDB引擎会在适当的时候,将这个操作记录更新到磁盘里面。 与此类似,InnoDB的redo log是固定大小的,比如可以配置一组4个文件,每个文件的大小是1GB,也就是总共4GB。从头开始写,写到末尾又回到开头循环写。 有了redo log,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe。
重要的日志模块:binlog
- redo log是InnoDB引擎特有的日志
- binlog是Server层的日志
这两种日志有以下三点不同
- redo log是InnoDB引擎特有的;binlog是MySQL的Server层实现的,所有引擎都可以使用。
- redo log是物理日志,记录的是"在某个数据页上做了什么修改";binlog是逻辑日志,记录的是这个语句的原始逻辑,比如"给ID=2这一行的c字段加1"。
- redo log是循环写的,空间固定会用完;binlog是可以追加写入的。"追加写"是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
深色表示在执行器中执行,浅色表示在INNDB内部执行。
将redo log的写入拆成了两个步骤:prepare和commit,这就是"两阶段提交"。
两阶段提交
两阶段提交就是为了让两份日志之间的逻辑一致。 由于redo log和binlog是两个独立的逻辑,如果不用两阶段提交,要么就是先写完redo log再写binlog,或者采用反过来的顺序。
六、内存篇
1. 揭开Buffer Pool的面纱
为什么要有Buffer Pool?
MySQL的数据是存储在磁盘里的,如果每次都从磁盘里面读取数据,这样性能是极差的。为了提高查询性能,InnoDB在存储引擎层设计了一个缓冲池(Buffer Pool),来提高数据库的读写性能。
所以,当数据从磁盘中取出后,缓存内存中,下次查询同样的数据的时候,直接从内存(buffer pool)中读取。
有了缓冲池后:
- 当读取数据时,如果数据存在于Buffer Pool中,客户端就会直接读取Buffer Pool中的数据,否则再去磁盘中读取数据
- 当修改数据时,首先是修改Buffer Pool中数据所在的页,然后将其页设置为脏页,最后由后台线程将脏页写入到磁盘
Buffer Pool有多大?
Buffer Pool缓存什么?
InnoDB会把存储的数据划分成若干个页,以页为磁盘和内存交互的基本单位,一个页的默认大小为16KB。因此,Buffer Pool同样需要按页来划分。 在 MySQL 启动的时候,InnoDB 会为 Buffer Pool 申请一片连续的内存空间,然后按照默认的16KB的大小划分出一个个的页, Buffer Pool 中的页就叫做缓存页。此时这些缓存页都是空闲的,之后随着程序的运行,才会有磁盘上的页被缓存到 Buffer Pool 中。 Buffer Pool 除了缓存「索引页」和「数据页」,还包括了「undo 页」,「插入缓存」、「自适应哈希索引」、「锁信息」等等。
查询一条记录,就只需要缓冲一条记录吗?
不是的。当我们查询一条记录时,InnoDB会把整个页的数据加载到Buffer Pool中,因为,首先通过索引定位到磁盘中的页,再通过页里的页目录去定位到某条具体的记录。
如何管理Buffer Pool?
为了更好的管理这些在Buffer Pool中的缓存页,InnoDB为每一个缓存页都创建了一个控制块,控制块信息包括[缓存页的表空间、页号、缓存页地址、链表节点]等等。
如何管理空闲页?(Free链表)
为了能够快速找到空闲的缓存页,可以使用链表结构,将空闲缓存页的控制块作为链表的节点,这个链表称为Free链表(空闲链表)。
空闲页:每当需要从磁盘中加载一个页到Buffer Pool中时,就从Free链表中取一个空闲的缓存页,然后把缓存页对应的控制块的信息填上,最后把该缓存页对应的控制块从Free链表中移除。
如何管理脏页?(Flush链表)
设计Buffer Pool除了能提高读性能,还能提高写性能,也就是更新数据的时候,不需要每次都写入磁盘,而是将Buffer Pool对应的缓存页标记为脏页,然后再由后台线程将脏页写入到磁盘。 为了快速知道哪些缓存页是脏页,就设计出Flush链表。它与Free链表相似,区别在于Flush链表的元素都是脏页。
如何提高缓存命中率?
LRU链表管理的初心:尽量高效的提高Buffer Pool的缓存命中率 简单的LRU算法的实现思路是:
- 当访问的页在Buffer Pool里,就直接把该页对应的LRU链表节点移动到链表的头部
- 当访问的页不在Buffer Pool里,除了要把页放入LRU链表的头部,还要淘汰LRU链表末尾的节点
简单的LRU算法并没有被MySQL使用,因为简单的LRU算法无法避免下面这两个问题:
- 预读失效
- Buffer Pool污染
什么是Buffer Pool污染?
小林coding
怎么解决出现 Buffer Pool 污染而导致缓存命中率下降的问题?
小林coding
脏页什么时候会被刷入磁盘?
引入了Buffer Pool后,当修改数据时,首先是修改Buffer Pool中的数据所在页,然后将其页设置为脏页,但是磁盘中还是原数据。 因此,脏页需要被刷入磁盘,保证缓存和磁盘数据一致,但若每次修改数据都刷入磁盘,则性能会很差,因此一般都会在一定时机进行批量刷盘。
可能大家担心,如果在脏页还没有来得及刷入到磁盘时,MySQL 宕机了,不就丢失数据了吗? 这个不用担心 InnoDB的更新操作采用的是Write Ahead Log策略,即先写日志,再写入磁盘。通过redo log日志让MySQL拥有了崩溃恢复的能力。
🎯总结
MySQL的数据是存储在磁盘里的,如果每次都从磁盘里面读取数据,这样性能是极差的。 为了提高查询性能,InnoDB在存储引擎层设计了一个缓冲池(Buffer Pool),来提高数据库的读写性能。
有了缓冲池后:
● 当读取数据时,如果数据存在于Buffer Pool中,客户端就会直接读取Buffer Pool中的数据,否则再去磁盘中读取数据
● 当修改数据时,首先是修改Buffer Pool中数据所在的页,然后将其页设置为脏页,最后由后台线程将脏页写入到磁盘InnoDB通过三种链表来管理缓存页:
- Free链表,来管理空闲页
- Flush链表,来管理脏页
- LRU链表,来提高缓存的命中率
它设计了一种高效的LRU链表,将链表分为young区和old两个区域,加入缓冲池的页,优先插入old区域;当页被访问的时候,才加入young区,目的是为了解决预读失效的问题。