一、SQL语句的执行流程

总体来说,mysql可以分为Server层和存储引擎层两个部分。
Server层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖mysql大多数核心服务功能,以及所有的内置函数(日期、时间、数学、加密函数等),所有跨存储引擎的功能都在这一层是实现,比如存储过程、触发器、视图等。
存储引擎负责数据的存储和提取。其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎。创建表的时候create table可以通过engine=memory来指定存储引擎创建表。
下面按照客户端到sql的执行,走一遍流程。

1.连接器

从客户端连接到数据库,需要与server层的连接器交互。

  1. 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.执行器

执行之前,首先会判断一下你有没有权限对该表执行这个操作。

image.png

日志模块

(InnoDB引擎中)如果是更新语句,或者删除语句还会涉及到两个重要的日志模块:redolog、binlog

Redolog

只有InnoDB存储引擎才会有Redo log.
image.png
image.png
redolog相当于是这里的粉板,用来分担大文件binlog的压力。
每当有更新语句时,InnoDB引擎会先把其记录到redo log, 并更新到内存。(redolog的目的就是因为binlog太大,读写比较慢,所以搞了一个小的log)
InnoDB引擎会在适当的时候将这个操作记录到磁盘里面,并修改binlog 文件。
redo log 的文件大小是固定的,比如可已配置4个1GB文件,循环写入。
image.png
write pos是当前写入的位置,checkpoint 是当前要擦除的位置。
在循环写入过程中,如果write pos追上checkpoint表示redolog满了,如果checkpoint追上write pos表示redo log 空了。

binlog

binlog只用于归档。
image.png

二、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,因为之前这条数据是没有的。
极客时间Mysql - 图6
下面如果一个线程过来修改了一下这个数据,也就是事务B,把值改成了值N,事务B的id是2,那么此时更新之前会生成一个undo log记录之前的值,然后会让roll_pointer指向这个实际的undo log回滚日志,如下图所示。
极客时间Mysql - 图7
多个事务串行执行的时候,每个人修改了一行数据,都会更新隐藏字段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,如下图所示
极客时间Mysql - 图8
接着呢,此时两个事务并发过来执行,一个事务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的,说明事务开启之前,修改这行数据的事务早就提交了,所以此时可以查询到这条数据。
极客时间Mysql - 图9
此时,事务B开始动手了,它把这行数据的值修改成了值N,然后这行数据的txr_id自然就是事务B的id,同时roll_pointer指向了修改之前生成的一个undo log,接着这个事务B就提交了,如下图所示。
极客时间Mysql - 图10
这个时候事务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更新的值,只会读到更早的值。
极客时间Mysql - 图11
接着假设事务A自己更新了这行数据的值,改成J,trx_id也就是20,同时保存之前事务B修改值的快照,如下图所示
极客时间Mysql - 图12
此时事务A查询这条数据的值,会发现这个trx_id=20,和自己的ReadView里的creator_trx_id是一样的,说明这行数据就是自己修改的啊!自己修改的值当然是可以看到的了!
接着在执行事务A的过程中,突然开启了一个事务C,这个事务的id为40,然后它更新了那行的值为K,还提交了,如下图所示
极客时间Mysql - 图13
这个时候事务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事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。
所以**, 如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。**

七、死锁

image.png
当出现死锁以后,有两种策略:

  1. 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout来设置。
  2. 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事 务得以继续执行。将参数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个间隙锁。这样就确保了无法再插入新的记录。