1.执行原理
今天我们来说说mysql,作为目前使用最为广泛的数据库,mysql中有很多值得我们深挖的技术和思想。
首先,先来聊聊一条sql语句是如何运行的。
sql执行原理.png
①与MySQL建立的数据库连接线程发起读取请求
②SQL接口处理发送过来的SQL语句
③SQL解释器解析SQL
④查询优化器查找最优路径
⑤执行器执行计划
⑥存储引擎执行SQL语句
这就是一条SQL语句进入数据库之后的执行流程,这里需要注意的是,在我们的数据库中,会先从内存中寻找数据,如果在内存中没有发现目标数据,再从磁盘中寻找数据;那么问题又来了,数据库是如何把数据加载进内存中的呢?以及它又是如何快速查找数据的呢?
带着这些问题,我们接着往下看!
2.执行引擎
这里我们主要讲两种引擎,在面试过程中,一般情况也只会涉及这两种引擎,其他的引擎(如Memory、Archive、Federated),感兴趣的可以做相关了解
1.MyIASM引擎
这个是MySQl默认的执行引擎,它支持全文索引,但是不支持数据库事务,也不支持行级锁。所以,对于查询业务来说,效率极高。反之,对于插入和更新操作,由于是表锁,效率相对来说较慢。
2.InnoDB引擎
前方高能!!!(重点了解)
作为最为常用的数据库引擎,InnoDB引擎有很多特性值得我们了解。在讲它底层存储结构之前,我们必须先了解一下数据是如何读取进入内存的。
Buffer Pool:可以看做是数据库的内存组件,里面缓存了磁盘上的真实数据,我们对数据库的增删改查操作,其实就是对这个内存数据结构中数据的操作。
读取过程:数据库运行时,首先会向操作系统申请一片内存区域,一般指定innodb_buffer_pool_size参数,然后将数据加载进入这片内存区域,也就是Buffer Pool中,这里需要注意,放入时一般是以数据页的形式存放的,什么意思?简单来说,就是我们每一条数据会放在一页上面,这个数据页一般大小是16KB,数据页上一般包含一些页面描述信息,如所属表空间、数据页编号、所属内存地址位置等,在存放时会判断哪些数据页是空闲的(可以存储数据),这个时候就需要依靠一个名为——free链表的家伙,它是一个双向链表数据结构,当数据加载进来的时候,如果发现已经存在在数据页上了,则会直接找相应数据,如果没有的话,从free链表中找到一个空闲的缓存块,从磁盘中读取数据页进入缓存块,然后删除free链表中这个描述数据块。
淘汰机制:当数据不断加载进入buffer pool ,最终free链表没有空闲之后,这时如果还需要加载数据的话,就必须淘汰一些缓存数据了。那么InnoDB是怎样实现淘汰数据的呢?我们这里引入缓存命中率的概念,当一个数据经常性的被访问,做增删改查操作,我们称这条数据的缓存命中率很高,反之,则缓存命中率很低。我们已经知道,当数据加载进入缓存页时,先是在free中拿出一个空闲的缓存块存储需要加载的数据,当数据加载完这个缓存块,把它从free链表删除,此时,我们会这个缓存块的描述信息放入LRU链表,表示已经加载了数据的缓存页,这时就可以通过LRU链表执行我们的淘汰策略了。怎么执行呢?见名思义,当命中缓存页时,我们就将改缓存页放在LRU链表头部,由此可见,如果某些数据一直没有命中,自然而然就到了链表尾部了,当需要进行淘汰时,优先淘汰LRU链表尾部缓存页。
存在问题:MYSQL预读机制规定,当一个区的多个数据页加载进入buffer Pool时,这时MySQL认为后续数据页也需要加载进入缓存池,此时就会触发预读机制(这个机制是通过参数 innodb_random_read_ahead 来控制的,他默认是 OFF),把后面相邻的数据页都加载进入缓存中;但是这时如果遇上一个数据量较大的全表扫描,那么就会淘汰掉很多之前存在的热数据,显然,这种设计就显得不合理了
传统LRU链表改进与优化: 冷热数据分离
将LRU链表分为两部分,一部分是热数据,一部分是冷数据,冷热数据的比例是由 innodb_old_blocks_pct 参数控制的,默认是 37,在初次加载数据进入缓存页是,一般把缓存页放在冷数据头部,当规定时间内缓存命中,则提到热数据头部,由此,即可解决热数据被全部替换的问题。定时清理冷数据尾部的缓存页刷入磁盘。
此处,我们顺便讲解一下刷入磁盘的过程:当数据更新时,我们并不会第一时间将数据刷入磁盘中,而是将更新的数据写入redo.log日志文件中,然后通过redo.log二进制文件顺序写入磁盘,而在加载缓存页时,内存是进行随机读写数据的,顺序读写的性能是非常高的
3.索引
好了,终于讲到我们的重头戏了——索引
从上面所介绍的,不难发现我们的数据是存储在一页一页的数据页上的,相邻的数据页会采用双向链表的格式相互引用,然后数据页上根据主键大小存储一条条数据,每一行数据都会有指针指向下一行数据,那么我们是如何快速找到这些数据的呢?答案就是索引!而建立索引的物理结构就是B+树。所以,想了解索引,说白了,我们就是在研究这个B+树是如何一步一步建成的。接下来,需要介绍两个名词~
页目录:每个数据页都会有一个页目录,里面顺序存放了每行数据的主键以及对应存放数据的槽位。
索引页:每个数据页的页号,还有数据页中最小的主键值放在一起,组成了一个索引的目录。
有关数据页的名词其实还有一些,这里知道这两个足以帮助我们理解索引了。从这两个名词中不知道各位有没有发现一点什么呢?
对的,我们要查询数据,最粗鲁的办法无非就是遍历链表找到对应主键,但是现在,我们有了页目录,索引页这种有规律的顺序结构,我们是不是可以利用二分法来进行查询呢?到此,我们对索引应该有了一个清晰的概念。在这颗B+树的底部,也就是叶子节点,存放的便是我们所有的数据页,通过双向链表进行关联,往上,就是我们索引页了,索引页与数据页之间会有指针引用关联,当一个索引页存储不下时,这时就会进行也分裂,从树的角度来说,就是分裂节点。(脑海中是不是浮现一颗正在分裂节点的多叉索引树呢)
运用索引
说完这个索引树的建立过程,我们再来说说如何有效的利用索引
一般来说,我们在新增数据时,默认是以主键作为索引,这个是聚簇索引,当我们建新的索引时,会重新根据新的索引排序规则建立一颗索引树,但是这个索引属于二级索引。什么意思?就是说这个索引树可以运用索引查找到你想要的数据对应的主键,然后我们拿着这个主键再进行回表查找到整条数据。
索引使用规则:
①基于等号匹配,一定会用上索引
②最左侧列匹配
③最左前缀匹配原则
④前置%无法利用索引
原则:尽可能的保证每一条SQL都可以用上索引,使用基数比较大的字段。
4.MVCC机制
通过索引,我们可以快速找到我们想要的数据,那么问题又来了,数据是得到了,但是当有多个事务(线程)同时都想同一资源,这时又会出现什么情况,以及我们又是如何解决这些情况的呢?
首先,我们讨论多线程读取数据出现的情况:
1.脏读——事务A读取事务B修改的数据,此时事务B还未提交事务并且产生回滚,事务A再次读取数据时,发现数据为NULL
2.幻读——事务A查询语句,结果集为十条数据,同时事务B插入两条数据,A再次查询时发现十二条数据
3.不可重复读——事务A执行查询语句,同时事务B、C对该数据进行修改,(前提——读已提交事务)事务A再次查询,发现结果不一致
由此,我们的设计人员提出了MVCC(多版本并发控制机制),专门处理多个事务并发运行引起的问题。这里,我们还有必要介绍一下SQL标准对事务的4个隔离级别,分别是:
read uncommitted ——读未提交
read committed ——RC 读已提交
repeatable read ——RR 可重复读(MySQL默认隔离级别)
serializable ——串行化
虽然MYSQL默认级别是RR,但是这个RR和SQL标准中的有所不同,MYSQL的RR级别下是不会发生脏写,脏读,幻读,不可重复读的。下面我们具体分析一下其原理——MVCC机制
MVCC机制的实现主要包括以下几部分组件:
undo log日志链表——记录每一次事务的详细情况(主要包括数据、事务值、指向下一事务的指针),以链表形式存储
readview机制——对每一次事务生成一个数据快照(主要包括正在实行的事务数组、最大事务id+1,最小事务id,正在执行的事务id),在RR级别下,对于读操作,一个事务中只会产生一个readview(RC情况下每一次查询都会生成一个新的readview),所以,这就可以有效防止不可重复读了。
下面详细介绍一下undo log日志链表和readview快照是如何配合工作实现MVCC机制的!
话不多少~直接上图
MVCC实现原理.png
当事务A与实务B同时操作一条数据时,在事务未提交之前,我们会根据事务先后顺序,生成一个带事务id的版本链,也就是我们的undo log日志链表,首先,事务A执行读取操作生成readview快照,根据快照中的信息在undo log链表中遍历查找,发现txr_id = 70的这条数据是在事务A之后产生的,即不符合规则,以此类推,直到找到符合规则的那条undo log日志,即事务A的读取结果。
5.锁机制
MVCC机制可以解决脏读,幻读,不可重复读等情况,但是它处理不了脏写。这时候就需要一种锁机制来解决这个问题了。
脏写——多个事务同时更新同一行数据
锁,顾名思义,就是在事务进行更新的时候,我们对这条数据进行上锁,不允许其他人做更新操作,请注意!是更新操作。在这里,我们同样介绍两种锁:
共享锁:也叫读锁,简称S锁
排它锁:也叫写锁,简称X锁,互斥锁,独占锁
依靠锁机制,我们可以让多个事务更新一行数据是串行化,避免同时更新,在事务更新时,先判断该行数据是否上写锁,如果上锁,则进入等待直至解锁,唤醒该事务。如果没有,这个事务会创建一个锁(包含这个事务id和等待状态),然后把这个锁和这行数据进行关联。
这里,我们的读锁和写锁是不会互斥的,由mvcc机制也可以看出,在事务更新时,我们的读取操作可以在undo log日志上面进行,也就是说,mvcc可以解决频繁加锁互斥的问题
6.性能调优
1.书写SQL语句时尽量要利用索引
2.尽量写简单的SQL,复杂的逻辑用代码去实现
3.能单表连接尽量不要多表关联
4.能多表关联尽量不要写子查询
5.最好是选择主从架构
6.能读写分离最好读写分离
面试总结系列第二面——欢迎留言讨论,共同进步!
网友评论