一条sql语句的执行流程
select * from t where id=1
1.mysql执行一条查询语句的流程
1.1客户端输入用户名密码连接mysql服务器
1.2查询这条sql语句有没有对应的缓存,如果有直接返回结果,如果没有执行以下流程
1.3语法分析器,
1.4词法分析器,这个主要是mysql要识别出比如说select是查询,t是个表名,id是个字段等
1.5优化器,在mysql中执行一条语句可以有很多执行计划,比如说可以用a索引,也可以用b索引
再比如说两表连接查询的时候a join b可以让a当驱动表,先从a表中查出符合条件的数据到b表中匹配,也可以反过来,优化器就是做这些事情,大多数情况下会选出最优解,但是也有例外
1.6上面的那些流程都是在mysql的server层执行的,然后server层会调用引擎的查询接口,innodb引擎看该用户是否有相应的权限,如果有权限就判断需要查询的数据是否在内存中,比如说id=5这行数据,如果在内存中就直接返回,如果不在内存中就去磁盘中把id=5这行数据所在的整个数据页都读入到内存中,然后下次查询这个数据页所有的数据时,就不用读磁盘了
一条update语句的执行流程
例子update t set name=xxx where id=3
这节课主要有两个日志
一个是binlog,一个是redolog
binlog日志叫做归档日志,也叫做逻辑日志
它是mysql本来就有的,主要就是用来记录sql语句的逻辑,比如说上面那条更新语句,如果binlog的格式是statement的话,binlog就会忠实的记录下来这条语句。
redolog这个日志是物理日志,它是innodb引擎特有的,它记录的是在这个数据页上哪个数据做了改动。
这条update语句在执行到调用引擎的那个地方,会先写入binlog日志,然后这个时候处于准备状态,然后调用引擎,引擎写好redolog日志后通知server层,这个时候才能提交,也就是说这两个日志用了两阶段提交,也就是说事务,保证写入这两个日志同时成功或者失败。
还有就是会把这条更新语句更新的最新数据写入到内存中,也就是说这条更新语句在写好了两个日志和内存后就直接告诉客户端更新成功了,其实磁盘中这行数据还是老值,然后这个内存页就会被称为脏页,那么什么时候会把redolog记录的更新操作应用到磁盘中呢,有这么四个时机
1.redolog写满的时候,这个redolog是固定大小的,可以通过一个参数设置,所以如果这个redolog写满了,就会把记录应用到磁盘上,这个时候内存页就是干净页了,还有一点就是binlog是没有固定大小的,它是追加写,写完了一个文件,继续写下一个文件,所以说这里就有个关于优化的问题,如果redolog设置的太小就很容易写满了,那么就会造成mysql频繁的刷盘,性能也就不好了。
2.mysql会有一个线程在后台执行,它如果觉得mysql很闲,比如说这个时候连接不多,或者执行的语句不多,就会刷redolog的日志应用磁盘上,这个时候一般不会关注性能问题,因为这个时候mysql很闲。
3mysql关闭的时候,这个时候也不会关注性能问题
4,淘汰的内存页是一个脏页,这个是什么意思呢,从头开始说吧,mysql从磁盘读出来的数据都是放在内存中的,那么这块内存就需要管理,名字叫做innodb_buffer_pool,这块内存里面存的数据是用链表的形式,当然也是有固定大小的,可以通过参数设置的,既然有大小,那么就会有淘汰,比如说读取一页数据,就会把这个数据页挂在链表头部,如果这块内存容量不够了就会把链表尾部的数据页给删掉,这其实就是lru算法,这个时候如果删掉的这个内存页如果是个脏页的话就会进行刷盘了,把脏页的数据应用到磁盘中。
join语句的流程
select a.name,b.sex from a join b on a.id=b.aid where a.id in(1,2,3)
这是两表连接,如果b表的aid字段建立了索引,那么这个时候会用到bnl算法,也就是性能最好的算法,它就是先从a表找出符合条件的一行行数据,在每查出一行数据后会去b表匹配,因为b表的这个aid字段建立了索引,这个时候不用对b表进行全表扫描,走的是索引,所以很快,如果b表的aid没有建立索引,这个时候会退化成另一种算法,执行流程如下,首先会把驱动表所有符合条件的数据查出来放入join_buffer中,看名字就看出来了,这块内存是专门用来连接的时候放临时数据的,然后放进去后就会和b表的数据进行匹配,这个时候就是全表扫描了,当然这块join_buffer是有固定大小的,如果驱动表的数据太多了放不下怎么办呢,很简单就是分段放,比如驱动表有十条数据,join_buffer大小只够放五条数据,那么就会先把驱动表的前五条数据放进内存然后和被驱动表匹配一次,接下来把剩下的数据在放进内存中,在和被驱动表匹配一次,这个时候也有个关于性能的问题,就是如果join_buffer设置的太小,就会造成多次和被驱动表匹配,性能自然不好。
binlog的三种模式
第一种模式是statement,这种模式下执行什么样的sql语句,binlog都会忠实的记录下来。
第二种模式是row,如果是这种模式,对于更新语句来说,会记录下来这条更新语句前后的数据,对于删除语句会记录下来删除的数据,对于添加语句也会记录下来添加的数据。
第三种模式是mixed,也就是混合,如果设置为这种模式,mysql会判断这条语句适合用哪种模式就用哪种模式,是混合的。
这三种模式最不合理的是statement,如果你的数据库的binlog设置为这种模式基本上可以认为是不合理的,最起码要改成混合模式,为什么说statement模式不合理呢,看下面这条语句,delete from t where a>=4 and b<=3 limit 1
这条语句优化器选择a索引和b索引所删除的数据是不一样的,所以说这条语句就会造成主备不一致,如果在主库上用的是a索引,但是这条binlog传到备库开始执行的时候优化器如果选择b索引,那么主备库数据就不一致了,所以说statement是不合理的,如果设置为mix模式,数据库就会自动判断,它如果觉得这条语句会引起主备不一致,那么就会使用row模式,反之会用statement模式,现在大多数用的还是混合模式比较多一点,因为row模式占用空间太大了,想一下,如果删除一个表的数据,statement仅仅会记录一条sql语句而已,但是row模式会记录下整表数据,但是用row模式的趋势也是很大的,因为这种模式恢复数据方便,想想如果你修改错数据或者删除错数据了,直接恢复即可,因为这种模式会记录下所有的数据,这也就是空间换时间,主要还是看你的服务器空间够不够大,如果够大的话,建议用这种模式。
事务的隔离级别
读未提交
读提交
可重复读
串行化
这四种隔离级别捂的严实的程度依次增加
但是性能依次降低
读未提交的意思就是在我的事务中修改某条数据,还没有提交,这个时候另外一个事务正好查询了这个数据,那么就能看到我的事务中修改的结果了,举个栗子,在a事务中执行update goods set kucun=kucun+1 where goods_id=1,这行数据的原库存值是1,更新以后就会变成2,
那么在a事务还没有commit的时候,在b事务中执行一条查询语句select kucun from goods where good_id=1,那么这个时候查出来的值就是2,这个就是读未提交隔离级别下会出现的情况。
读提交的意思就是b事务的查询的结果就是原值1,只有等到a事务提交以后,b事务在查询的值就是2。
可重复读隔离级别的意思就是说无论其他事务有没有对查询的数据进行修改,对于我这个事务来说,我不管,我只管在我这个事务开始的那一刻,在整个库中所有已经提交的事务的数据,在我这个事务执行期间,我所看到的数据从开始到结束都是一致的,也就是说b事务查询的这条数据始终是1,不管a事务有没有提交,当然这只是针对快照读,啥意思呢,比如说这条语句改成select kucun from goods where good_id=1 for update 这时候就是当前读了,当前读是啥意思呢,意思就是要读出最新的数据,还有就是如果在b事务执行对这条数据的更新语句的话,也要读出最新数据,更新语句必然是当前读,因为它更新要在最新的数据版本上进行修改,这个其实就涉及到了,数据多版本。其实就是在数据库中每个数据有多个版本,还是说说更新语句吧,商品表中库存值初始值是0,在a事务中执行了一条更新语句对这个库存值加1,在b事务中把这个值又加了1,这个时候变成了2,在c事务中又加了1变成了3,那么数据库就会记录下这个更新的记录,也就是undolog,在数据库中每个事务都是有一个id的用来标识这个事务,也就是事务id,undolog记录的是啥玩意呢,其实就是大概这个样子,我们假设a b c事务的事务id分别是1,2,3那么kucun=1 是有事务id为1的事务修改的,kucun=2是由事务id为2的事务修改的,这就是回滚日志,它会一直保存到整个数据库中没有比它还早的事务的时候才会删除,这句话啥意思呢,举个栗子,比如说有个a事务执行时间很长,从上午9点会一直执行到下午五点,然后在这个a事务中呢,执行了一条查询语句select kucun from goods where goods_id=1,然后呢在a事务执行期间呢,有个b事务执行了update goods set kucun=3 where goods_id=1,然后呢又有个c事务也对这条数据的库存值进行了修改,那么在a事务提交之前,bc事务进行修改的这两个undolog都要保存下来,直到a事务提交后这些undolog才会删除掉,所以说在事务中我们尽量要减少长事务,也就是说一个事务的执行时间不要太长,上面的这个例子是极客时间上这篇对应文章下面的一个评论,我当时看到这个评论后马上就理解了这个意思,很感谢这个评论的人,到现在为止这条评论对我来说还是印象挺深刻的。
串行化隔离级别下的意思就是串行,哈哈,就是一步一步执行,读会加读锁,写会加写锁,也就是说b在执行这条查询语句的时候会被锁住,一直等到a事务提交以后才能查询这条数据,结果就是2。
接下来在说说在一个事务中语句执行顺序的问题
比如说在一个事务中执行了abc这三条语句,执行顺序就是abc,当然这三条语句都要加锁,那么在执行到a语句的时候对a语句涉及到的数据进行加锁,执行到b语句的时候对b语句涉及到的数据进行加锁,c也是一样,那么这些锁在什么时候释放呢,答案是事务提交的时候,也就是说这三条语句a锁涉及到的数据加锁时间最长,b次之,c最短,释放锁不是在语句执行完以后就释放,而是在事务提交以后所有的锁才释放,那么在我们排列语句顺序的时候要怎么排列呢,我们需要把涉及到热点表数据的语句放在顺序的最后面,这样对热点数据的加锁时间就会少一点,减少锁等待的情况。
死锁检测
这个其实就是多个事务由于锁出现了互相等待的现象,从而造成了死循环,跳不出来了,那么出现了这种情况该怎么办呢,mysql有两种处理方案,第一种是等,等一定的时间后把第一个事务踢出去,这个一定的时间是可以设置的,默认值是50秒,踢出去以后别的事务才有机会继续执行,还有一种方案是死锁检测,mysql如果判断出现了死锁的现象,就会把造成死锁的事务踢出去,这块有点记不清楚了,大概是这么个意思,默认是第二种方案。
索引分为主键索引和普通索引
主键索引简单来说就是主键下面跟着对应的行数据,普通索引也叫二级索引,存的就是主键值,啥意思呢,比如说一个表里面有主键id name sex字段,每个主键索引存的就是这一整行数据,而如果对name字段设置索引,那么这个索引就是普通索引也就是二级索引,这个索引存的值就是主键id的值,举个栗子如果一条sql语句是这样的select * from t where name=帅哥 这个时候如果用到了name这个索引,那么mysql会在这个索引上找到主键id的值,然后在去主键索引上找到这一行数据,这个就叫做回表,但是如果语句改成了select id from t where name=帅哥,那么就不需要回表了,因为这个语句只需要查询主键id这个字段,这个主键id已经在name索引上有了,这个其实就是覆盖索引,覆盖索引的意思就是需要查询的字段在索引上已经存在了,在举个联合索引的例子,比如我们要建立一个name age的联合索引,然后我们执行 select name age from t where name=帅哥 and age=23,这个时候也不需要回表,也就是覆盖索引,因为这个语句我们用到了这个联合索引,并且我们需要查的字段在这个联合索引上都有,最后在说说索引下推的概念,这个索引下推的功能好像是在5.6版本以后才有的,比如说我们要执行select name age from t where name=帅哥 and age<23,如果没有索引下推,那么在执行这条语句的时候虽然会用到name这个索引,但是在判断年龄的时候还需要回表一次,也就是需要通过主键id去主键索引上查询出这条数据的年龄是不是符合年龄小于23这个条件,如果有索引下推的话,就直接在这个联合索引上直接进行年龄判断了,因为你这个联合索引里面本来就有年龄呀,为啥还要回表一次去查年龄呢。
排序
select name age from t order by age desc
排序有两种方式,第一种叫做全字段排序,第二种叫做rowid排序,然后还有一个参数,这个参数规定了排序中单行字段的最大值,比如说这个最大值是6,然后name+age的类型总数值小于这个单行最大值,那么就会走全字段排序,全字段排序就是把需要排序的数据,针对上面这条语句就是把表中所有的数据的name和age字段放进sort_buffer中,然后直接对age字段排序,sort_buffer的大小也是由一个参数设置的,就是sort_buffer_size,如果需要排序的数值超过了sort_buffer的大小,那么就会用到临时文件来排序,用临时文件排序的话用到了归并排序的算法,就是把需要排序的数据分成好几个临时文件排序,最后在把这几个临时文件合并起来。
然后如果单行字段的值超过了规定的单行字段最大值就会走rowid排序,这种排序就是把age和id字段放进sort_buffer中,然后根据age字段排序好以后,这个时候id的值也是有序的了,然后根据这些id的顺序到表中查出name age的值。
binlog和redolog的提交方式
binlog的提交方式有三种,有一个参数可以设置
就用x代替这个参数,如果x=0,每次提交事务只把binlog记录到内存中,如果x=1,每次提交事务会把binlog记录到硬盘中,如果x大于n,n大于1,就是累计n个事务才会记录到硬盘中,放进硬盘以前是存在操作系统的文件系统缓存中的,设置成这种方式的风险就是在执行期间如果宕机会导致这几个事务的binlog丢失,redolog也大概是这个意思(有点忘了这块),binlog和redolog都有组提交的概念。
主备怎么同步数据
主库执行完一个事务后会把这个事务的binlog同步到备库,备库会开两个线程,一个线程用来接收主库的日志,接收以后放在中转日志里面,也就是备库用来放这个日志的地方,然后还会有一个线程用来执行这个日志,这里其实就会有主备不一致的地方,比如说主库执行一个事务需要十五分钟,那么主库执行完以后把这个日志传给备库执行,这之间就会有十五分钟的时间差,这个时间差是怎么算出来的呢,我们把主库执行完事务的时间记为t1,备库接收完日志的时间记为t2,备库执行完日志的时间记为t3,t3-t1就是这个时间差,也就是延迟时间,那么这个主备延迟会带来什么问题呢,或者说叫主从延迟更合适一点,比如说在一个电商平台里,作为商家要上传商品,我上传商品执行的插入语句是在主库完成的,插入完以后从库还没执行完这个日志,这个时候我在从库查询这个商品就会查不到,那么怎么解决这个问题呢?
1,sleep方案,主库执行完以后让程序睡一会,差不多从库就执行完了,这种看起来不靠谱,但是这种最简单,有的时候你看起来不靠谱,其实用的还是挺多的。
2,对平台里的数据进行分析,分出来哪些是必须要保证不能延迟的,哪些是可以容忍有一些延迟的,比如说用户修改的个人资料,这种数据就可以容忍有一些延迟,对于不允许延迟的数据就强制走主库,对于可以容忍有一些延迟的就走从库。
3,gtid方案,mysql对于每一个执行完的事务都会分配一个gtid,全局递增的,mysql提供了一个接口,可以通过这个gtid去查询这个事务有没有在从库执行完,好多细节都忘了