一、SQL语句的执行流程
总体来说,mysql可以分为Server层和存储引擎层两个部分。
Server层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖mysql大多数核心服务功能,以及所有的内置函数(日期、时间、数学、加密函数等),所有跨存储引擎的功能都在这一层是实现,比如存储过程、触发器、视图等。
存储引擎负责数据的存储和提取。其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎。创建表的时候create table可以通过engine=memory来指定存储引擎创建表。
下面按照客户端到sql的执行,走一遍流程。
1.连接器
从客户端连接到数据库,需要与server层的连接器交互。
mysql -h$ip -P$port -u$user -p
一个用户成功建立连接后,即使你用管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置。
show processlist可以查看连接状态。
2.查询缓存
如果是select 查询请求,则会先去查询缓存中查看之前是不是执行过这条语句。
由于缓存命中率很低,mysql8.0之后已经将查询缓存删除。
3.分析器
词法分析->例如分析select、insert,分析某个词是表还是字符串
语法分析->判断sql是否满足mysql语法
如果语句不对,返回You have an error in your SQL syntax
4.优化器
优化器是在表里面有多个索引的时候,决定去走哪一个索引;或者一个语句有多表关联的时候决定各个表的连接顺序;或者where中的比较顺序,自动按组合索引靠左的顺序排序。
5.执行器
执行之前,首先会判断一下你有没有权限对该表执行这个操作。
日志模块
(InnoDB引擎中)如果是更新语句,或者删除语句还会涉及到两个重要的日志模块:redolog、binlog
Redolog
只有InnoDB存储引擎才会有Redo log.

redolog相当于是这里的粉板,用来分担大文件binlog的压力。
每当有更新语句时,InnoDB引擎会先把其记录到redo log, 并更新到内存。(redolog的目的就是因为binlog太大,读写比较慢,所以搞了一个小的log)
InnoDB引擎会在适当的时候将这个操作记录到磁盘里面,并修改binlog 文件。
redo log 的文件大小是固定的,比如可已配置4个1GB文件,循环写入。
write pos是当前写入的位置,checkpoint 是当前要擦除的位置。
在循环写入过程中,如果write pos追上checkpoint表示redolog满了,如果checkpoint追上write pos表示redo log 空了。
binlog
二、MVCC
undo log 版本链
在数据库中每行数据都有两个隐藏字段(存在InnoDB的聚簇索引中),一个是trx_id,一个是roll_pointer,这个trx_id就是最近一次更新这条数据的事务id,roll_pointer就是指向当前这个事务之前生成的undo log。
比如,现在一个事务A(事务id=1),插入了一条数据,那么此时这条数据的隐藏字段以及指向的undo log如下图所示,插入的这个数据的值是X,因为事务A的id是1,所以这条数据的txr_id就是1,roll_pointer指向一个空的undo log,因为之前这条数据是没有的。
下面如果一个线程过来修改了一下这个数据,也就是事务B,把值改成了值N,事务B的id是2,那么此时更新之前会生成一个undo log记录之前的值,然后会让roll_pointer指向这个实际的undo log回滚日志,如下图所示。
多个事务串行执行的时候,每个人修改了一行数据,都会更新隐藏字段trx_id和roll_pointer,同时之前多个数据快照对应的undo log,会通过roll_pointer指针串联起来,形成一个重要的版本链!
ReadView
当执行一个事务的时候,就会生成一个对应的ReadView,从redolog读取,并放在内存中,里面比较关键的东西有4个
- 一个是m_ids,这个就是说明此时有哪些事务在Mysql里执行还没有提交
- 一个是min_trx_id,就是m_ids里最小的值
- 一个是max_trx_id,mysql下一个要生成的事务id,就是最大事务id
- 一个是creator_trx_id,就是你这个事务的id
假设原来数据库里就有一行数据,很早以前就有事务插入过了,事务id是10,它的值为M,如下图所示
接着呢,此时两个事务并发过来执行,一个事务A(id=20),一个事务B(id=30),事务B是要去更新这行数据的,事务A是要去读取这行数据的值的。
现在事务A直接开启了一个ReadView,这个ReadView里的m_ids就包含了事务A和事务B的两个id,20,30,min_trx_id就是20,max_trx_id就是31,creator_trx_id就是20,是事务A自己。
这个时候事务A第一次查询这行数据,会走一个判断,当前这行数据的txr_id是否小于min_trx_id,此时发现txr_id=10m,是小于min_trx_id的,说明事务开启之前,修改这行数据的事务早就提交了,所以此时可以查询到这条数据。
此时,事务B开始动手了,它把这行数据的值修改成了值N,然后这行数据的txr_id自然就是事务B的id,同时roll_pointer指向了修改之前生成的一个undo log,接着这个事务B就提交了,如下图所示。
这个时候事务A再次查询,此时查询的时候,当查询到数据N行时,其中的trx_id=30,那么txr_id是大于ReadView里的min_txr_id(20),同时小于ReadView的max_trx_id的,说明更新这条数据的事务,很可能就跟自己差不多同时开启的,于是查看这个txr_id是否在ReadView的m_ids列表中,如果在列表中,这个修改数据的事务是跟自己同一时段并发执行然后提交的,所以对这行数据是不能查询的!只能查到trx_id等于10的数据。多个事务并发执行的时候,事务B更新了值,通过Read view+undo log日志链条的机制,就可以保证事务A不会读到并发执行的事务B更新的值,只会读到更早的值。
接着假设事务A自己更新了这行数据的值,改成J,trx_id也就是20,同时保存之前事务B修改值的快照,如下图所示
此时事务A查询这条数据的值,会发现这个trx_id=20,和自己的ReadView里的creator_trx_id是一样的,说明这行数据就是自己修改的啊!自己修改的值当然是可以看到的了!
接着在执行事务A的过程中,突然开启了一个事务C,这个事务的id为40,然后它更新了那行的值为K,还提交了,如下图所示 
这个时候事务A再去查询,会发现当前数据的trx_id=40,大于了自己的ReadView中的max_trx_id,此时说明什么?
说明是这个事务A开启之后,然后有一个事务更新了数据,自己当然是不能看到的了!
此时就会顺着undo log多版本链条往下找,自然先找到值A自己之前修改的过的那个版本,因为那个trx_id=20跟自己的ReadView里的creator_trx_id是一样的,所以此时直接读取自己之前修改的那版本。
通过undo log多版本链条,加上你开启事务时候生产的一个ReadView,然后再有一个查询的时候,根据ReadView进行判断的机制,你就知道你应该读取哪个版本的数据。
而且它可以保证你只能读到你事务开启前,别的提交事务更新的值,还有就是你自己事务更新的值。假如说是你事务开启之前,就有别的事务正在运行,然后你事务开启之后 ,别的事务更新了值,你是绝对读不到的!或者是你事务开启之后,比你晚开启的事务更新了值,你也是读不到的!
通过这套机制就可以实现多个事务并发执行时候的数据隔离。
三、Read Committed隔离级别是如何基于ReadView机制实现的
当一个事务处于RC隔离级别的时候,它每次发起查询,都重新生成一个ReadView!
四、ReadView是如何实现RR隔离级别的
Mysql的RR隔离级别,能够同时避免不可重复读和幻读问题。RR隔离级别下,在事务中,第一次查询生成ReadView,在这个事务中查询都会使用这个ReadView,但是更新是基于最新值进行更新。
具体的案例实现,参考CSDN博客https://blog.csdn.net/filling_l/article/details/112854716
五、覆盖索引、回表、最左前缀、索引下推
六、行锁
并不是所有的引擎都支持行锁,InnoDB支持行锁。
在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。
所以**, 如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。**
七、死锁

当出现死锁以后,有两种策略:
- 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout来设置。
- 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事 务得以继续执行。将参数innodb_deadlock_detect设置为on,表示开启这个逻辑。
在InnoDB中,innodb_lock_wait_timeout的默认值是50s。
假设有1000个并发线程要同时更新同一行,那么死锁检测操作就是100万这个量级的。
怎么解决由这种热点行更新导致的性能问题呢?
1. 如果你能确保这个业务一定不会出现死锁,可以临时把死锁检 一测关掉。
2. 是控制并发度。
八、count(*)执行效率
MyISAM引擎把一个表的总行数存在了磁盘上,因此执行count()的时候会直接返回这个数, 效率很高;
而InnoDB引擎就麻烦了,它执行count()的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数。(因为MVCC的原因)
九、间隙锁
在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。因此, 幻读在“当前读”下才会出现。
而间隙锁就是来解决幻读的。
顾名思义,间隙锁,锁的就是两个值之间的空隙。比如文章开头的表t,初始化插入了6个记录, 这就产生了7个间隙。
这样,当你执行 select *fromt where d=5 for update的时候,就不止是给数据库中已有的6个记 录加上了行锁,还同时加了7个间隙锁。这样就确保了无法再插入新的记录。
