MySQL的执行流程#

如上图所示,MySQL的架构分为两大部分:
-
server层:负责mysql的连接、查询、解析、执行
-
存储引擎层:Innodb、MyISAM
那么执行一条select 语句,期间到底发生了什么?
1. 连接#
在执行sql之前,我们首先要启动sql服务并连接上mysql,这是最基础的步骤。连接的时候经过TCP三次握手。
我们可以通过show processlist命令来查看有多少的客户端连接了MySQL
一般我们都使用长连接,短连接需要频繁连接mysql,但是长连接累计过多也会导致服务占用的内存过大,浪费资源,很有可能导致mysql重启。所以为了解决长连接占用内存的问题,我们可以:
2. 查询缓存#
连接之后,客户端向sql服务端发送sql语句,mysql收到sql语句后会解析sql的第一个字段,然后就会判断是不是select,如果是就会查找缓存中有没有数据。
查询缓存是个比较鸡肋的功能,MySQL8.0 之后就没有了查询缓存
-
查询缓存的存储方式是key-value的形式
-
每次表进行更新操作的时候,查询缓存都会被清空
3. 解析sql#
sql的解析是通过解析器来完成的
解析器会做两件事情:
4. 执行sql#
mysql的执行分为三个阶段:
MySQL 一行记录是怎么存储的?#
MySQL的数据存放在哪个文件?
MySQL的表结构存放在 table.frm 文件
MySQL的表数据存放在 table.ibd 文件
mysql表文件结构#

-
行:数据库表中的记录都是按行进行存放的
-
页:表中的记录存储在数据页里面(B+树那个页表)
-
区:连续的64个页划分到一个区内,这样方便范围查找,每个区大小1MB
-
段:段是由多个区组成,分为数据段、索引段、回滚段
Innodb的行格式 👈 重点#
行格式是一条数据的存储的结构
InnoDB 存储引擎设计了 4 种不同类型的行格式 Compact、Redundant、Dynamic 和 Compressed 行格式。innodb默认的行格式为Dynamic
-
Redundant 行格式不是紧凑型的,基本没人用了
-
dynamic和compressed 都跟 compact 非常像,所以重点讲解compact
Compact行格式#


记录的额外信息#
- 变长字段长度列表:char(n) 是固定大小,varchar(n) 类型的数据大小不固定,变长字段所占的字节数会按照逆序的方式存放,变长字段所占空间大小跟允许存储的最大字节大小有关系,如果小于255字节,那么就会用1字节表示,如果大于255字节,分两种情况:如果变长字段实际长度小于128字节那么就是占用1字节,如果大于等于128字节就用2字节表示。
小于128字节:长度字段的最高位为0,低7位直接存储实际长度值。
大于等于128字节:长度字段最高位为1,低7位存储后续用于表示长度的字节数量,后续字节的最高位均为1,低7位存储具体长度值。 1
例如:
- NULL值列表:NULL值不存放在列中,存放在NULL值列表中,并且存放的方式也是跟变长字段长度列表一样,逆序存放,但是存储的时候必须按照整个字节的位来表示,高位补0,下面有图示。null值列表默认1字节空间,NULL值列表的大小是由字段的数量决定的(所有字段都是NOT NULL的话NULL值列表不会存在),9个字段后就会创建2字节的空间,以此类推


- 记录头信息:例如:delete_mask 、next_record、record_type
记录的真实数据#

记录的真实数据前面有三个隐藏字段:
-
row_id : 如果数据指定的有主键或者是有唯一性约束列,那么就不会有这个字段,如果没有的话,就会生成一个隐藏的id字段 ,占6字节
-
trx_id:表示这个数据是哪个事务生成的,trx_id是必需的,占6字节
-
roll_pointer:指向上一个版本,roll_pointer 是必需的,占7字节
varchar(n) 中 n 最大取值为多少?(这里只考虑ascii情况下)#
mysql的一条记录除了TEXT、BLOBs类型的列,限制最大为65535字节,(包含NULL值列表和变长字段长度列表)
字符集是 ascii 的情况下1 个字符占用 1 字节
-
对于单个字段来说,最大的varchar(n)取值就是当NULL值列表的大小为0时(字段NOT NULL)此时由于允许的最大存储长度 > 255,所以变长字段长度列表占2字节,也就是 65535 - 2 = 65533
-
如果有多个字段的话,要保证所有字段的长度 + 变长字段字节数列表所占用的字节数 + NULL值列表所占用的字节数 <= 65535。
行溢出后,MySQL 是怎么处理的?#
上面我们计算过,除了TEXT、BLOB类型的数据可以存放大对象,varchar(n) 类型的数据最多存放65532大小的数据,所以如果遇到大对象,很容易发生溢出现象,compact行结构的处理方式是,记录的真实数据处只会保存该列的一部分数据,然后把剩余的数据放在溢出页中,在真实数据处留出20字节的空间,指向溢出页的位置。

Compressed 和 Dynamic 这两个行格式和 Compact 非常类似,他们采用完全的行溢出方式把所有的数据都放进溢出页,真实数据记录处只存储20个字节的数据指向溢出页

索引#
什么是索引?#
索引是提升查询速度的一种数据结构
-
索引之所以能提升查询效率,是因为在插入的时候对数据进行了排序
-
缺点:影响了插入或者更新的性能,并且建立索引占用磁盘空间
索引分类#
Innodb下的B+树索引:主键索引是聚簇索引,非主键的是二级索引,二级索引叶子节点存放的是主键值,我们在使用索引查找的时候,如果查询的二级索引,正好包含所需要的列,那么就是覆盖索引
-
根据数据结构划分:B+tree索引、Hash索引、Full-text索引。
-
根据字段特性划分:主键索引、唯一索引、普通索引、全文索引
-
根据物理存储划分:聚簇索引和非聚簇索引
-
作用字段个数划分:单列索引和联合索引
索引组织表和堆表#
MYISM的存储是堆表。Inoodb是索引组织表
数据存储有堆表和索引组织表两种方式。
-
堆表:堆表中的数据是无序的,数据的排序依赖索引,索引的叶子节点存放的是指向堆表的数据地址
-
索引组织表:根据主键排序,叶子节点存放数据
-
除主键索引外的索引都是二级索引,索引的叶子节点存放的是(索引键值,主键值)
联合索引#

联合索引遵循最左匹配原则:
-
规则1:MySQL从联合索引最左边的索引列开始匹配,从左到右顺序匹配,如果中间没有用到某个列,那该列后面的列都无法使用联合索引
-
规则2:如果联合索引左边的列使用了范围查询,那么该列右边的所有列都没法用联合索引
索引下推#

如何提高联合索引的效率#
-
我们尽量避免查询后的范围查询,因为这样会导致查询到的结果需要重新排序
-
利用组合索引包含多个列的特性,实现索引覆盖,提升查询效率
索引失效#
模型数空运最快or
-
左模糊或者左右模糊匹配:因为索引按照索引值有序排列,只能通过前缀比较
-
数据类型转换:如果字段是字符类型,我们输入的是整形会走全表扫描
-
对索引使用函数:索引保存是元素的原始值,而不是经过函数计算过的值
-
对索引进行表达式的计算:类似使用函数
-
联合索引非最左匹配
-
or是联合索引的时候,如果or后面的不是索引列,or后面的查找不会走索引,而是索引下推
如何优化索引#
-
避免回表操作,我们可以使用覆盖索引来对二级索引进行优化
-
主键索引最好是自增的,这样我们在插入的时候不用对数据进行移动
-
索引最好设置为非空:首先,count函数会忽略null值,其次,省去了记录额外信息空间中的NULL值列表的空间。
-
防止索引失效
优化器是如何选择合适的索引#
MySQL的优化器默认是CBO优化器,一条 SQL 的计算成本计算如下所示:
Cost = Server Cost + Engine Cost
= CPU Cost + IO Cost
MySQL有两个表,一个是server_cost,一个是engine_cost,里面记录了对于各种成本的计算
为什么会出现不使用索引的情况呢?
- 全表查询的速度比索引快(回表的次数多就可能直接全表扫描了)
创建索引的标准#
-
要考虑回表的问题
-
考虑索引失效
-
一般只对高选择度的字段创建索引,低选择度的字段如性别,不创建索引
-
低选择性,但是存在数据倾斜,可以考虑通过创建索引查询少量的数据
-
如果数据倾斜严重,可以创建直方图,让优化器知道索引中数据的分布,校准查询计划。
可能适用于索引的数据结构#
二分查找#
- 索引数据按照顺序排列,每次查找我们都能通过二分查找高效的查询数据(时间复杂度 O(logn)
由于二分查找需要索引按照顺序线性排列,那么就会导致插入的时候需要大量移动索引,在磁盘上对数据进行操作,那么效率会大大的降低
二分查找树#
- 与普通的二叉树不同的是,二分查找树的元素特点是 :左子树 < 根节点 < 右子树
非常容易链化,导致树的高度非常高,严重影响效率
自平衡树(AVL)#
-
解决了二分查找树的痛点,特点是树的左子树和右子树的高度不能超过1
-
通过自旋来满足左右子树高度不超过1
不管是红黑树还是自平衡树,他们随着插入元素的增加, 树的高度也就增加,这就导致磁盘i/o次数增加,影响效率
B树#
-
B树的结构是为了降低树的高度推出的,他不再限制每个节点的子节点数量
-
B树的每个节点都存放的有数据,每次我们通过索引查询的时候,那些我们不需要的数据也能查到,这就浪费了很多内存空间
-
我们在进行范围查询的时候会涉及多个节点的磁盘i/o,使得查询效率大大降低
B+树#
-
B+树的所有数据都存放在叶子节点上
-
叶子节点构成一个有序链表,而且叶子节点本身按照关键字的大 小从小到大顺序链接。所以范围查询的时候比B树要更好
回表#
回表是指根据索引查询到的主键值再去访问主键索引,从而获取完整的数据记录。
回表操作通常会导致访问两次索引,增加IO次数,影响效率
导致回表的情况#
-
索引不包含查询的字段
-
使用了非聚簇索引
-
使用的覆盖索引,但是超过了索引的最大长度(767字节)
如何避免出现回表#
-
使用聚簇索引
-
使用覆盖索引的时候,注意索引的长度,不要超过767字节
索引面试技巧#
问题1:什么是索引?#
索引的本质就是数据结构。从数据结构上来说,mysql 的索引主要是B+树索引,它的查询性能好,还能进行范围查询。我们也可以通过不同的角度对索引进行划分:比方说根据叶子节点是否包含数据划分为 聚簇索引和非聚簇索引;根据覆盖的列分为单列索引和联合索引
问题2:为什么使用B+树作为mysql的数据结构?#
-
如果我们使用二分查找树的话,插入的数据全是逐渐增大的,会导致链化,查询效率降低
-
如果使用自平衡树或者是红黑树的话,插入的时候,由于会自旋,会导致插入效率变慢,并且随着数据量的增加,树的高度也会逐渐增加,导致查询的效率大大降低
-
跳表在极端情况下会退化为链表,平衡性差,并且跳表需要更多的内存。
-
如果使用B树,那么我们在查询的过程中,会查询到我们不需要的数据。并且B树对范围查询并不友好
事务#
事务的四大特性(ACID)#
-
原子性:事务的操作要么全部完成,要么全都不做,不会操作的过程中终止
-
一致性:事务操作完成后,数据的状态保持一致
-
隔离性:将事务隔离,防止多个事务交叉执行,对一个数据进行读写,出现数据不一致的情况(避免事务穿插执行)
-
持久性:事务完成后对数据的修改是永久的,在磁盘上保留了下来,不会因为系统故障导致数据丢失
原子性是undo log回滚日志来保证的
一致性是持久性+原子性+隔离性来保证
隔离性是MVCC或者锁机制来保证的
持久性是redo log 日志保证的
并发事务带来的问题#
-
脏写:事务A修改了事务B未提交的数据
-
脏读:事务A读取了事务B未提交的数据,此时事务B如果回滚,数据就会无效
-
不可重复读:事务A读取了某个字段,事务B修改了这个字段,事务A 再去读取的时候,前后的数据不一致
-
幻读:事务A 读取了某个字段,事务B 此时新增了一个字段,事务A 再去读取的时候,多了一个字段
不可重复读和幻读的区别在于:不可重复读是数据值的变化,而幻读是新增或者删除了数据
MySQL的事务隔离级别#
MySQL默认隔离级别是可重复读
-
读未提交:可以读到未提交的数据
-
读已提交:事务只能看见已经提交的事务的数据,解决了脏写和脏读的问题但不能解决不可重复读和幻读
-
可重复读:事务A读到一条数据,此时事务B修改了这个数据,那么事务A读到的内容和修改前的内容是一致的,可以解决不可重复读,但是幻读不能完全解决
-
可串行化:隔离级别最高,事务串行化执行,可以避免脏读、不可重复读、幻读
快照读和当前读的区别#
-
快照读:快照读是依赖数据的历史版本(快照)来实现的非阻塞的操作,他是通过MVCC实现的(select语句就属于快照读)
-
当前读:读取数据的最新版本
MVCC(多并发版本控制)#
实现原理:Read View、隐藏字段、undo log
隐藏字段#
我们在介绍MySQL的 compact行格式 的真实数据中提到过,有两个隐藏的字段

-
trx_id:记录最后一次修改改行数据的事务id
-
roll_pointer:回滚指针就会指向该行数据的上一个版本。形成undo log版本链
undo log 数据版本链#
每行数据的多个版本通过 roll_pointer 串联成链表,最新版本在链头,旧版本依次向后。
Read View#
Read View 有四个字段: creator_id 、m_ids 、min_trx_id、max_trx_id
-
creator_id <创建read>创建read> View 的事务id
-
m_ids :存放了活跃的事务id
-
min_trx_id:最小的活跃事务的id
-
max_trx_id:指的是应该创建下一个事务的id(全局事务id+1)

Read View最主要的作用就是判断当前事务执行快照读的时候,哪个版本对他可见。
数据对当前事务的可见性判断:
可见性判断的核心是 “事务提交状态” 和 “事务启动顺序” 的结合,其中 “是否提交” 是基础(未提交的版本一定不可见),而 “启动顺序” 则决定了已提交版本在不同隔离级别下的可见范围。
-
数据中隐藏字段中的 trx_id 如果等于当前 事务id,可见
-
数据中隐藏字段中的 trx_id 比活跃事务列表中的 事务id 小,说明该数据已经有事务提交过了,那么数据对当前事务可见
-
数据中隐藏字段中的 trx_id 大于下一个创建的事务id,说明该数据是在当前事务后生成的事务,那么数据对当前事务不可见
-
数据中隐藏字段中的 trx_id 在 min_trx_id 和 max_trx_id 之间,并且活跃列表中也没有隐藏字段中的 trx_id,说明该版本数据是对当前事务可见,否则不可见
可重复读和读已提交的区别#
-
可重复读的级别下,事务在进行快照读的时候会创建一个Read View ,但是后续再进行快照读的时候,他会用第一次创建的Read View来判断可见性,所以,即使中间有其他事务修改并提交了数据,当前事务读到的数据依然是原来的
-
在读已提交的隔离级别下,事务每次进行快照读的时候都会创建一个Read View,所以该隔离级别下,即使中间有其他事务修改并提交了数据,该事务依然可以读取到修改后的数据
总结:两者的区别在于创建Read View 的时机不同
可重复读:只在第一次快照读的时候创建
读已提交:每次进行快照读的时候都会创建
可重复读为什么没有完全解决幻读?#
快照读如何解决幻读#
因为每次进行快照读的时候,我们都会使用第一次快照读创建的Read View 所以不会出现前后读取数据不一致的情况
当前读如何解决幻读#
MySQL除了普通的快照读,其他都是当前读,例如:update、insert、delete等语句执行前都会查询最新版本的数据,再进行下一步操作。
Innodb为了解决当前读情况下每次都会查询新版本的数据而造成的幻读问题,使用了间隙锁
当数据被事务A 插入next-key lock 后,事务B进行插入操作的时候,会加一个意向锁,从而进入等待状态,直到事务A提交之后, 事务B 才会执行
可重复读出现幻读的情况#

除了上面这一种场景会发生幻读现象之外,还有下面这个场景也会发生幻读现象。
-
T1 时刻:事务 A 先执行「快照读语句」:select * from t_test where id > 100 得到了 3 条记录。
-
T2 时刻:事务 B 往插入一个 id= 200 的记录并提交;
-
T3 时刻:事务 A 再执行「当前读语句」 select * from t_test where id > 100 for update 就会得到 4 条记录,此时也发生了幻读现象。
要想避免发生幻读,就要在事务开始之后立马加上next_key lock(间隙锁和记录锁)避免其他事务插入新的数据
锁相关#

锁的原理和存储#
MySQL 中的锁(尤其是 InnoDB 引擎的锁)并非直接 “加到数据本身”,而是基于索引加锁。具体来说,锁的作用对象是索引记录(或索引间隙),通过索引关联到对应的数据行,从而间接控制对数据的访问
-
索引本身不存储锁信息,而是通过其键值(或范围)作为 “标识”,让锁表知道 “锁定的是哪部分数据”。
-
锁的具体信息(如类型、所属事务、范围等)存储在 InnoDB 内部的锁表中,通过索引键值关联到具体的数据行或间隙。
当需要判断锁冲突时,InnoDB 会通过索引键值到锁表中查询:如果其他事务已对相同索引键值持有冲突的锁(如 X 锁与 S 锁冲突),则当前事务需要等待。
锁的分类#
全局锁#
MySQL全局锁加上后,数据库处于只读状态
-
优点:全库备份的时候使用,防止备份时数据变化导致备份前后数据不一致
-
缺点:数据处于只读状态,业务停滞
表级锁#
表锁#
表锁可以限制当前线程和其他线程对某个表进行读写操作,表锁的粒度太大,影响并发性能
元数据锁(MDL锁)#
元数据锁是自动加上的,当一个线程对数据库表进行操作的时候,那么MySQL会自动加上元数据锁
-
当我们对一个表中的行进行CRUD操作的时候,加的是MDL读锁(表级的锁,不是行锁,也就是说这个锁只会限制事务是否可以访问这个表,限制行的是行级锁)
-
当我们对表结构做变更的时候,加的是MDL写锁,MDL写锁直接限制了事务对表的访问
元数据锁在事务提交之后自动释放
如果数据库中有一个长事务 A 正在执行,此时线程 B 对表结构做变更操作的时候由于一直获取不到写锁,会导致后续的select 请求被阻塞,原因是:申请 MDL 锁的操作会形成一个队列,队列中写锁获取优先级高于读锁,一旦出现 MDL 写锁等待,会阻塞后续该表的所有 CRUD 操作。
意向锁#
作用:快速判断表内是否有记录被加锁
-
声明锁意图:事务在申请行级锁(S/X锁)前,需先在表级加对应的意向锁(IS/IX锁),表明“计划对表中部分行加锁”
-
快速冲突检测:其他事务申请表级锁时,通过检查意向锁即可判断是否存在行锁冲突,无需遍历所有行数据
对记录加锁之前要先在表上加上意向锁(意向共享锁、意向独占锁)
当执行插入、删除、修改操作的时候,需要先对表加上意向独占锁,然后再对该记录加独占锁,普通的select不会加锁,因为有MVCC保证一致性读
这样在加锁的时候就不用遍历整个表来判断是否有记录已经加锁
AUTO-INC 锁#
在插入数据的时候,会加一个表级别的AUTO-INC 锁,然后为 自增的字段赋予递增值,等插入语句执行完毕后这个锁就会被释放。
在加了AUTO-INC 锁之后,其他线程插入数据的时候都会被阻塞,从而保证自增字段的连续递增
由于插入的数据过多的时候,会导致阻塞,影响性能,在MySQL5.1.22版本开始,Innodb就提供了一个轻量级的锁,每次插入的时候,会为自增字段加上AUTO-INC锁,为该字段赋值一个自增数据后该锁就会被释放,不会到插入执行完毕后再释放。
有一个字段可以控制AUTO-INC 锁的粒度 innodb_autoinc_lock_mode
-
innodb_autoinc_lock_mode = 0 就采用 AUTO-INC 锁,语句执行完毕后再释放
-
innodb_autoinc_lock_mode = 1 的时候,普通插入一条数据的时候是 赋值自增数据后释放,但是在执行批量插入的时候还是在语句执行完毕后释放锁。
-
innodb_autoinc_lock_mode = 2 的时候是赋值自增数据后就释放
行级锁#
InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁。
Record lock#
记录锁,锁住的是一条记录,记录锁满足多个事务同时加锁,但是只能是共享锁之间叠加,独占锁不行。
| 共享锁 | 独占锁 | |
|---|---|---|
| 共享锁 | 兼容 | 不兼容 |
| 独占锁 | 不兼容 | 不兼容 |
Gap lock#
间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。间隙锁是兼容的,两个事务可以同时拥有相同范围的间隙锁。锁住一个范围,范围是双开区间。
Next-Key lock#
是间隙锁和记录锁的结合,可以锁住该记录和范围内的记录,有一个闭区间,由于他有记录锁,所以存在互斥关系。
插入意向锁#
一个事务在插入数据的时候需要先判断插入位置是否被加了间隙锁(next-key lock也有间隙锁),如果有就会被阻塞,于是就加一个插入意向锁,表明有事务想在某个区间插入新记录,但是现在处于等待状态。
⭐MySQL是如何加行级锁的#
参考资料:MySQL 是怎么加锁的?我是看了这篇文章后,按自己的理解复盘总结了一下。
我们知道MySQL的行锁总共有三个:next-key、gap、record。在可重复读隔离级别下,MySQL通过加next-key锁来避免幻读,接下来我会分析MySQL是如何加锁的,以及如何通过加锁来解决幻读。
- 下面是我们演示的数据表:

-
查看加锁细节的方法如下:
-
开启一个事务
BEGIN -
执行你想执行的SQL:
select * from user where id = 5 for update -
执行
select * from performance_schema.data_locks查看加锁细节 -
记得提交事务
COMMIT
-
-
下面是对LOCK_MODE 的说明:
-
X:next-key 锁
-
X,REC_NOT_GAP<记录锁>记录锁>
-
X,GAP<间隙锁>间隙锁>
-
主键索引#
等值查询存在数据#

执行完毕后,得到的结果是:表级别的意向锁、行级别退化成了记录锁,锁的是主键为 5 的记录
等值查询不存在数据#

执行完毕后,得到的结果是:表级别的意向锁、行级别的间隙锁,锁的范围是(5,10) mysql是基于索引加锁的,id = 7 这个数据不存在,只能通过锁定已有的索引(5,10)这个范围。以此来限制别的事务插入 id = 7 的情况
范围查询 #

结果是:表级别的意向锁、行级别的next-key锁,锁的范围是(5,10] (10,15] (15,20] (20,+∞] mysql是基于索引加锁的,id = 6 这个数据不存在,只能通过锁定已有的索引(5,+∞] 这个范围。以此来限制别的事务插入 id > 6 的范围
总结一下,mysql是基于索引加锁的,我们要想加锁的数据不被修改,必须找到条件值附近的索引来加锁,例如 id > 6这个条件。我们的数据中没有id = 6的数据,于是要想锁住 id > 6 这个范围,必须找到id = 6前面的那条数据,也就是id = 5。然后加锁(5,+∞];同理,如果是锁 id < 6 的这个范围,就要找到id = 6 后面的那条数据 id = 10,然后加锁 [1,10)
二级索引#
存在值的等值查询#
我们在age字段上也加了普通的索引,我们试验一下,如果进行条件查询的时候会怎么加锁

InnoDB 的二级索引 age 实际存储的是 (age, id) 对,并按字典序排序。按照输出来说,我们锁住的是(15,20)这个组合的上下区间。也就是((1,19),(15,20)] + ((15,20),(5,21))

不存在值的等值查询#

可以看到,我们锁的是(39,20)的间隙锁,也就是范围 ((10,22),(20,39))
范围查询#

范围查询锁的都是下一个数据的临键锁,也就是我们只需要找到这个数据的下一个数据(包含),如果是大于就加下一个数据的临键锁和正无穷那个,如果是小于,就加上下一个数据的临键锁和小于这个值的所有值的临键锁
死锁#
死锁发生的原因#
两个或者多个事务执行过程中,因争夺资源造成互相等待,导致死锁。MySQL如果监测到死锁,会选择其中一个事务回滚,打破这个僵局
死锁产生的常见的场景#
场景一:行锁顺序不一致(最常见)#
事务A:
事务B:
-
事务 A 锁住了
id=1,事务 B 锁住了id=2。 -
事务 A 想锁
id=2,但被事务 B 占用; -
事务 B 想锁
id=1,但被事务 A 占用; -
形成循环等待,死锁发生。
✅ 解决方法:所有事务按相同的顺序访问资源(如都先更新
id小的记录)。
场景二:间隙锁或者next-key-lock#
可重复读隔离级别下,间隙锁如果出现重叠就会发生死锁,例如:间隙锁 (1,10) 和间隙锁 (5,15) 两者在重叠区域形成循环等待
场景三:没有命中索引#
如果没有索引,那么mysql会对每一条记录加上行锁,导致死锁概率增大
如何排查死锁#
使用命令来查询innodb引擎状态
这个命令会输出一个状态,里面有记录死锁相关的信息
-
发生死锁的时间
-
两个或者多个事务的详细信息
-
每个事务持有的锁
-
每个事务等待的锁
-
哪个事务被MySQL选择“牺牲”(回滚)
死锁怎么解决#
死锁不能完全解决,我们只能通过一些手段降低死锁发生的概率
-
减少事务执行的时间(缩短锁持有时间)
-
MySQL的锁释放时机是在事务提交之后,所以我们可以通过减少事务执行时间(缩短锁持有时间)
-
避免大事务,将大的事务拆分为小的事务
-
-
减少间隙锁:
- 如果可重复读和幻读对业务影响不大,把隔离级别调整为读已提交
-
给表加上合适的索引
- 因为不走索引MySQL会对每一行记录都加锁
-
MySQL参数调整
-
MySQL锁等待时间阈值参数,如果等待超过多久就会回滚
-
主动死锁检测,MySQL如果发现有死锁,就会回滚某一个事务
-
日志#
MySQL有三种日志:undo log(回滚日志)、redo log(重做日志)、binlog(归档日志)
undo log(回滚日志)#
undolog是回滚日志,保证了事务中的原子性
-
实现 MVCC(多版本并发控制)关键因素之一,MVCC是通过Read View + undolog实现的undo log为每条记录保存多个历史数据,当满足Read View的可见性后,会找到undolog对应的数据版本
-
实现事务回滚,在事务提交之前,MySQL会把更新前的数据存放到undolog,compact行格式中真实数据列表中的 roll_pointer 指针,就是指向存放在 undo log 中修改前的数据
Buffer Pool#

为了提高读写性能, Innodb 引擎设计了一个缓存池,当读取数据的时候可以先去缓存池中查找,当修改数据的时候同样可以在缓存池中修改(被修改过的页是脏页),为了减少磁盘的I/O,不会立刻写入,而是后续由后台线程找个合适的时机写入
Buffer Pool 除了缓存「索引页」和「数据页」,还包括了 Undo 页,插入缓存、自适应哈希索引、锁信息等等
redo log(持久化)#
Redo log 记录的是“在哪个数据页的什么位置做了什么修改”,是一种物理逻辑日志(Physical-Logical Log),用于在系统崩溃后重新应用(重放)这些修改,确保已提交事务的数据不会丢失。
记录更新到写入redolog经历了什么#
一条记录的更新,会先在内存中修改数据页,同时生成 redo log 写入 redo log buffer;当事务提交时,redo log 会根据配置(
innodb_flush_log_at_trx_commit)刷入磁盘的 redo log 文件,而数据页的持久化则由后台线程异步完成。
这个过程遵循 WAL(先写日志) 原则:必须先写日志,才能提交事务。
写入到 redo log 中依然需要刷新到磁盘中,这不是多此一举吗?
如果不写入redo log ,得先找到数据页再修改,较为随机(数据页位置)效率很低
写入到 Redo log 的数据是按顺序写入的,写入完成后事务就可以提交了。剩下的是交给操作系统处理
Undo log 脏页需要先找到数据的位置,再进行写入
MySQL的写磁盘操作由随机写变成了顺序写,提高了执行能力
- Redo log 记录了某个数据页做了什么修改,每当产生一条 redo log 时,会先写入到 redo log buffer,后续再持久化到磁盘
redo log buffer 的刷盘策略#
缓存在 redo log buffer 里的 redo log 还是在内存中,它什么时候刷新到磁盘?
-
MySQL正常关闭的时候
-
记录的数据大于 redo log buffer 空间的一半时
-
Innodb 后台线程每隔 1s 都会将数据持久化到磁盘
-
每次提交事务的时候都会将 redo log buffer 中的数据持久化到磁盘(默认),该策略可以由 innodb_flush_log_at_trx_commit 参数控制
innodb_flush_log_at_trx_commit 参数
-
参数为0:事务提交不会触发刷盘,每隔1s将redo log buffer 数据写入到磁盘。
-
参数为1:每次事务提交都会触发刷盘
-
参数为2:事务提交后,会将redo log buffer 数据写入到 redo log文件中,意味着数据被缓存到了操作系统缓存,然后每隔1s将操作系统缓存中的数据写入到磁盘
安全性:参数1 > 参数2 > 参数 0
性能: 参数0 > 参数2 > 参数 1
Redo log 文件满了咋办?#
Redo log 有个重做日志文件组,由两个redo log 组成

Redo log 循环写:先写第一个,再往第二个里面写。
脏页内容刷新到了磁盘后,redo log 中的数据也就无效了,可以擦除,用一个指针指向需要擦除的位置->check-point

-
write pos ~ checkpoint (红色): 空记录,用来记录后续新的操作
-
write pos ~ checkpoint (蓝色):待刷入到磁盘中的记录
如果 write pos 追上了checkpoint 说明空间不足了,此时MySQL会被阻塞,将蓝色部分刷新到磁盘中(脏页变成干净页),然后标记可擦除的地方,腾出空间,然后MySQL恢复,继续执行更新操作
Redo log 和undo log 的区别在哪儿?#
-
undolog是在事务执行之前记录的是更新之前的值
-
redolog是在事务执行之后记录的是更新之后的值
binlog#
Binlog 记录了MySQL数据库表结构变更和表数据的修改操作,不会记录查询。
思考:为什么有了binlog 我们还引入 redo log ?
Binlog 和 redolog 的区别#
-
适用的对象不同
-
binlog 是在server层,所有的存储引擎都可以调用
-
redolog 是Innodb引擎实现的日志
-
-
写入的方式不同
-
binlog 是追加写的方式,写完就新建一个写
-
redolog 是循环写的方式
-
-
文件格式不同
-
binlog 有三个格式类型:statement、row、mixed
-
redolog 是物理日志,记录了在某个地方做了什么修改
-
-
用途不同
-
binlog 用于备份恢复、主从复制
-
redolog 用于系统崩溃、停电数据恢复
-
如果数据库数据都被删了,能用redolog 恢复吗?
主从复制的过程#
MySQL的主从复制是依赖binlog实现的,复制的过程就是将主库的binlog传输到从库中。这个一般是异步实现的
-
MySQL主库收到客户端事务的提交请求后,先把数据写入binlog,然后提交事务,更新存储引擎中的数据,然后返回客户端操作成功的提示
-
从库会创建一个专门的 I/O 线程,连接主库的 log dump线程,来接收主库的binlog日志,接收的日志存放在从库的 relay log 日志中(相当于一个缓存),然后接收完毕,返回给主库复制成功的响应。
-
从库会创建一个来回放(执行binglog里面的sql)binlog 日志的线程,读relay log日志,回放binlog更新存储引擎数据,实现主从一致性
在完成主从复制之后,就可以在写数据时只写主库,在读数据时只读从库,这样即使写请求会锁表或者锁记录,也不会影响读请求的执行。
从库不是越多越好,因为从库多了之后,主库连接log dump线程的I/O线程就会变多,对主库消耗大。一般是一主两从一主备
主从复制的模型还有那些?
-
同步复制:事务提交需要等待从库全部复制完毕后才会提交
-
异步复制(默认):后台I/O线程对接从库log dump线程进行主从复制
-
半同步复制:事务提交只要有一个从库复制完毕后就可以提交
binlog刷盘策略#
事务执行过程中,先把日志写到 binlog cache(Server 层的 cache),事务提交的时候,再把 binlog cache 写到 binlog 文件中。
事务的两阶段提交问题#
只要事务涉及数据修改(INSERT/UPDATE/DELETE)并且同时启用了
binlog和InnoDB,MySQL 在每次提交事务时都会执行“两阶段提交”(Two-Phase Commit, 2PC)。关键前提:必须开启了 binlog
为什么要两阶段提交#
MySQL中redolog保证持久性,binlog用于主从复制和数据恢复。如果一个事务只写了redolog没写binlog,会导致主库有数据,从库没有,或者是崩溃恢复后前后数据不一致
系统崩溃导致主从数据不一致的问题#
我们知道,mysql在事务提交之后需要将redo log日志和 binlog 日志持久化到磁盘中,但是如果中途出现宕机,那么就会导致主从数据不一致情况
假设执行了下面的语句:update t_user set name = xsong where id = 1
-
情况1:redo log持久化成功,binlog还没持久化,那么mysql恢复后,redolog会崩溃修复没有持久化的数据,将name=xsong 恢复到主库中,但是binlog没有被持久化,从库就拿不到name = xsong的数据,导致主从不一致
-
情况2:binlog持久化成功,redo log还没持久化,那么mysql恢复后,主库中name = niuniu的数据没有变化,但是binlog被持久化,从库会更新name = xsong,导致主从不一致
mysql在持久化的过程中,如果出现“半成功”的情况,都会导致主从不一致,所以mysql为了避免这种情况使用了两阶段提交
两阶段提交的过程#
两阶段提交把事务的提交拆分为了两阶段:Prepare阶段、Commit 阶段。MySQL在开启binlog 日志的情况下,为了保证binlog 和Innodb redo log 这两个日志的一致性,使用了MySQL内部XA事务,binlog作为协调者,存储引擎是参与者

过程如下:
-
Prepare阶段:将内部事务的id (XID)写入到redo log 中,把redo log 状态设置为 prepare,然后持久化redo log
-
Commit阶段:将内部事务的id(XID)写入到binlog中,把binlog持久化到磁盘,然后调用存储引擎提交事务的接口,将redo log的状态设置为commit ,binlog 写磁盘成功就会被认定为事务提交完成
解决主从不一致问题#
在 MySQL 重启后会按顺序扫描 redo log 文件,碰到处于 prepare 状态的 redo log,就拿着 redo log 中的 XID 去 binlog 查看是否存在此 XID:
-
情况1:如果 binlog 中没有当前内部 XA 事务的 XID,说明 redolog 完成刷盘,但是 binlog 还没有刷盘,则回滚事务
-
情况2:如果 binlog 中有当前内部 XA 事务的 XID,说明 redolog 和 binlog 都已经完成了刷盘,MySQL重启后就提交事务。
两阶段提交是由 binlog 写成功为事务提交的标识, binlog 写成功,就意味着能在 binlog 中查找到与 redo log 相同的 XID。
两阶段提交的问题#
-
磁盘 I/O 次数过多:每次事务提交都会进行两次刷盘操作,一次是 redo log刷盘,一次是binlog刷盘
-
锁竞争激烈:为了保证两阶段提交redo log 和binlog 的刷盘顺序,mysql使用了锁来保证原子性,在高并发的情况下,锁竞争会很激烈
两阶段提交就像数据库事务的”双保险机制”。当你执行事务提交时,MySQL会先把数据变更记录写入redo log,这时候redo log处于”prepare”状态,就像给数据变更买了第一份保险,确保即使此时数据库崩溃,重启后也能通过redo log恢复这部分变更。
接着,MySQL会将整个事务的逻辑操作记录到binlog,这相当于给数据变更办了”公证”,确保主从复制时从库能完整复现这个事务。
最后,当binlog写入成功后,MySQL才会把redo log的状态改为”commit”,完成整个事务提交。这样设计的好处是,无论在哪个阶段发生故障,都能通过这两个日志的状态对比来决定是回滚还是继续提交,从而保证主库和从库的数据一致性。我在处理主从同步问题时,经常需要通过分析这两个日志的状态来定位数据不一致的原因。
调优#
Explain#
EXPLAIN 语句用来查看某个查询语句的执行计划
执行计划输出各行详解#

-
id :在一个大的查询语句中每个 SELECT 关键字都对应一个唯一的 id
-
select_type :SELECT 关键字对应的那个查询的类型
-
table:查询中表的表名
-
*partitions: 匹配的分区信息
-
ref: 当使用索引列等值查询时,与索引列进行等值匹配的对象信息
-
rows: 预估的需要读取的记录条数
-
*filtered: 某个表经过搜索条件过滤后剩余记录条数的百分比
type 描述了找到数据所用的扫描方式#
system
当表中只有一条记录,并且该表使用的存储引擎是MyISAM,那么type类型是system但是换成Innodb就会变成 all,例子如下:


Const
当我们根据主键或者唯一的二级索引列进行等值匹配的时候,数据所用的扫描方式是const,例如:

eq_ref
在连接查询中,如果被驱动表是通过主键或者唯一二级索引列进行等值匹配的方式进行访问的,那么被驱动表的访问方法就是eq_ref

ref
通过普通二级索引列与常量进行等值比较的时候所用的扫描类型就是ref
range
使用索引获取某些范围区间的数据时,那么就可能使用到range
index
当我们可以使用索引覆盖,但需要扫描全部的索引记录时,该表的访问方法就是 index
possible_keys和key #
possible_keys:可能用到的索引;key :真正使用的索引
possible_keys列中的值并不是越多越好,可能使用的索引越多,查询优化器计算查询成 本时就得花费更长时间,所以如果可以的话,尽量删除那些用不到的索引。
key_len #
-
对于使用固定长度类型的索引列来说,它实际占用的存储空间的最大长度就是该固定值,对于指定字符集的 变长类型的索引列来说,比如某个索引列的类型是 ,使用的字符集是 utf8 ,那么该列实际占 用的最大存储空间就是 100 × 3 = 300 个字节。
-
如果该索引列可以存储 NULL 值,则 key_len 比 NOT NULL 时多1个字节。
-
对于变长字段来说,都会有2个字节的空间来存储该变长列的实际长度。
filtered#
-
如果使用的是全表扫描的方式执行的单表查询,那么filtered代表的是满足搜索条件的记录到底有多少条(如果总共有10条数据,只有1条满足要求那么filtered的值就是10)
-
如果使用的是索引执行的单表扫描,那么filtered代表的是满足除使用到对应索引的搜索条 件外的其他搜索条件的记录有多少条。
Extra#
extra列说明了一些额外的信息,接下来挑一些常见的额外信息来说明
-
No tables used:查询的语句没有from
-
Impossible where:where条件永远为false(例如:where 1 != 1)
-
No matching min/max row :查询的语句中使用了min或者max,但是不符合where语句中的条件
Using index
出现该信息,说明MySQL能使用覆盖索引来避免访问表的行,不会回表,可以提升查询效率。
Using where
MySQL将在存储引擎中检索行拿到数据后,再去server层进行过滤,索引下推可以很好的避免这个问题
Using index condition
该搜索条件中出现了索引列,但是可能因为索引失效导致没有走索引
Using filesort
查询语句中的排序操作无法使用索引,只能在内存(记录少的时候)或者磁盘(记录多的时候)中进行排序,统称为“文件排序”
Using temporary
查询中使用了内部临时表
SQL优化#

-
硬件优化:花钱买服务器硬件
-
操作系统优化:针对操作系统内核参数,例如I/O策略
-
数据库软件优化:例如调整事物的隔离级别、Innodb引擎的 日志刷盘时机等
-
SQL优化:优化SQL提升查询效率
SQL优化的目的:减少磁盘I/O、减少CUP消耗
-
减少磁盘I/O:尽量避免全表扫描、尽量使用索引、尽量使用覆盖索引避免回表
-
减少CPU消耗:尽可能减少排序、分组、去重之类的操作、尽量减少事务持有锁的时间
优化策略:

慢SQL日志如何打开?#
- mysql有一个查看慢日志的状态命令,慢日志默认是关闭的

-
我们也可以通过以下命令来打开或者关闭慢日志
-
我们还可以通过修改MySQL的配置文件 my.cnf 文件的配置来设置慢日志的属性
如何通过慢查询日志分析 #
我通常会先配置慢查询参数,设置long_query_time阈值和log_queries_not_using_indexes选项,确保能捕获到真正有问题的SQL。日志生成后,我习惯用pt-query-digest工具分析,它能帮我快速定位执行频率高、耗时久的SQL。找到目标SQL后,会结合explain查看执行计划,重点关注type列的访问类型和key列是否正确使用索引,再检查rows列的扫描行数是否合理。如果发现全表扫描或临时表问题,就会考虑优化索引或重写SQL。对特别复杂的SQL,我还会结合show profile分析具体执行阶段的耗时分布,这样能更精准地找到性能瓶颈点。
索引创建的策略#
大部分的慢SQL都是因为没有使用到索引,走全表扫描导致速度变慢
对列建立索引的时候我们可以参考以下原则:
-
我们一般使用高选择性的列来建立索引,低选择性例如:性别这种不建立索引
-
对于低选择性的,如果数据有倾斜的话,我们可以选择针对数据少的查询建立索引,如果有大量的数据倾斜还可以创建直方图来告诉优化器我们的数据分布
-
频繁出现在where中的列,这样可以避免全表扫描
-
频繁使用order by中的列,因为B+树索引是有序排列的
-
防止索引失效导致的全表扫描
慢SQL的优化#
优化数据的访问#
查询性能低下的最基本的原因就是访问的数据过多,很多数据都不是我们想要的,我们想要的数据只是从大量数据筛选出来的一小部分,我们可以通过以下两种思路来优化数据的访问:
-
是否向数据库请求了不需要的数据
-
MySQL是否在扫描额外的记录
请求了不需要的列
- 查询不需要的记录:例如写一个新闻网站查询新闻的接口,我们的前端页面只显示10条就够了,但是我们却查询所有数据给前端,让前端来过滤数据。
解决思路:加limit限制查询的条数
- 多表关联时返回全部的列:我们在对学生和成绩表进行关联的时候,我们只需要学生姓名和成绩这两列,但是查询的时候,我们获取了所有列
解决思路:使用 select s_table.name,r_table.core 这种,而不是使用select *
- 重复查询相同的数据:例如新闻网站评论的接口,对于用户的头像我们需要频繁查询,我们可以加缓存提高查询效率
扫描过多的数据
-
扫描行数和返回的行数:我们可以通过explain语句来查看sql执行计划,里面的rows 和 filtered 行可以表示
-
查看访问的类型:type列可以看到
如果我们的sql扫描了大量数据但是只返回了少量的行,我们可以使用以下办法来优化:
-
使用覆盖索引
-
重写查询
切分查询#
将大的查询切分为多个小的查询,例如:删除一千万条数据,我们可以分批删除,这样可以把一次性的压力分散到比较长的时间段中,可以大大减轻对服务器的影响
覆盖索引#
查询的时候,如果没有索引字段,那么考虑建立索引或者是联合索引,通过覆盖索引可以很好的避免回表现象
避免索引失效#
检查SQL语句中,有没有可能导致索引失效的语句
分解关联查询#
很多高性能的应用都会对关联查询进行分解,例子如下:

分解关联表的好处:
-
单表查询的话,可能命中查询缓存,提高效率
-
单个查询可以减少锁之间的竞争
-
在应用层做关联而不是在数据层关联,可以更容易对数据库进行拆分,更容易做到高可用和可扩展
-
减少冗余的记录,在应用层做关联,意味着只需要查询一次,而在数据层做关联可能会对同一个数据重复访问多次
排序优化#
如果extra中显示的内容是 filesort——文件排序,那么我们可以对排序进行优化,可以考虑对排序字段和其他字段建立联合索引,因为B+树索引是天然有序的
优化关联查询#
-
确保ON 或者 USING 后面的列上有索引,在关联顺序的第二个表的关联列建立索引即可
-
确保任何的GROUP BY 和 ORDER BY 中的表达式中只涉及一个表
优化子查询#
- 子查询尽量用关联查询代替
优化分页offset x limit y#
分页的场景下,我们使用 limit 10000,2 来实现分页,这种偏移量非常大的分页十分影响性能,因为查到的数据前10000条都要被抛弃。如果使用了二级索引,那么大量无效的回表会严重影响性能
优化方式一:减少扫描次数
对于这种偏移量超级大的分页操作,我们可以通过记录上一页最后一条数据的id(max_id),然后在下一页查询的时候加上where id > max_id,这样就可以保证偏移量永远是0,查询速度始终稳定在毫秒级
优化方式二:减少回表
原理:二级索引中存在主键id,所以我们可以先查到前10000个数据的id 加偏移量20,这样我们在查询id为 10020、10021…的数据
减少锁持有的时间#
案例1:调整sql 的执行顺序
我们在执行update操作的时候,MySQL事务会加行级锁——next-key lock,这个锁是事务提交之后才会释放,如果update 和select 之间没有什么依赖关系,也就是说他们的操作互不影响的时候,一般来说把加锁的语句放在事务靠后的位置执行
案例2:分批删除
删除大量的数据表数据的时候,最好采用分批删除的方法,因为删除的时候,mysql会给删除的数据加上行锁,直到全部删除完毕之后,才会释放。锁持有的时间很长,会影响效率。
WHERE 和 HAVING 优化#
-
where:group by 分组和聚合函数前过滤,不可以使用聚合函数过滤
-
having:group by分组和聚合函数后过滤,可以使用聚合函数

如果sql和索引都没问题,查询还是比较慢怎么办?#
-
分批查询:将大的查询分多次小查询
-
缓存:对于频繁查询的数据,我们可以放到redis缓存
-
分表:如果数据量很大,我们就可以考虑分表,通过减少总查询的数据量来解决查询缓慢的问题
-
主从复制:针对读多写少的情况,我们可以搭建MySQL主从模式来分摊读请求的流量
-
分库:针对写少读多的场景,单库无法抗住高并发流量,就要进行分库,把并发请求分散到多个实例
高可用#
分库分表#
什么是分库分表?
分库分表就是把原本存储在单张表上的数据分成多个表,把原本储存在同一个数据库上的数据分成多个库,实现数据切分。
分库分表使用场景:
-
当单张表中数据量过大的时候,一般是500w以上的数据,通过减少每次查询的数据量来解决查询缓慢的问题
-
当单台MySQL扛不住高并发流量的时候就要考虑分库了,把并发请求散发到多个MySQL中
分库分表带来的问题#
分布式事务一致性#
一个大的操作被拆分为多个小的操作,多个小的操作被放到多个服务器中,如何才能让这些操作要么都成功要么都失败。如何实现分布式事务一致性呢?
基于MQ的消息投递
全局ID唯一性
-
UUID
-
雪花算法
Snowflake 是 Twitter 开源的分布式 ID 生成算法,由 64 位的二进制数字组成,一共分为 4 部分,下面是示意图:

缺点:强依赖机器时钟,如果出现始终回拨可能会导致生成相同id
跨库跨表关联查询问题
- 数据全量存放到ES 中,通过ES查询
跨库跨表COUNT问题
-
把计数的数据单独存放到一张表里
-
数据全量存放到ES 中,通过ES查询
跨库跨表排序问题
-
业务代码分别查询每个库的数据,汇总排序
-
数据全量存放到ES 中,通过ES查询
MySQL数据同步ES的四种方法#
- 电商中常见的场景——MySQL数据同步Elasticsearch。
同步双写#
在写入MySQL的时候,直接也同步往ES里写一份数据。

-
优点:实现比较简单
-
缺点:
-
业务耦合
-
影响性能:写入两个存储,响应时间长
-
异步双写#

定时任务#
数据订阅#
MySQL通过binlog订阅实现主从同步,各路数据订阅框架比如canal就依据这个原理,将client组件伪装成从库,来实现数据订阅。
