ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MySQL八股浅析

MySQL八股浅析

SQL基础

数据库 (Database):是表的集合

MySQL架构

MySQL 整体可以分为两层:Server 层和存储引擎层。

首先客户端会通过 TCP 和 MySQL 建立连接,这一步由 Server 层中的连接器负责,它会做连接管理、用户认证以及相关的权限处理。

客户端发送 SQL 之后 会先作为key在查询缓存里面查 但是这个缓存记录在8.0版本后就删去了 (维护成本大于收益)

接着sql会经过server层的解析器进行词法分析、语法分析 生成AST语法树, 把 SQL 从字符串转换成内部能够理解的语法结构。

预处理器提前判断 SQL 中引用的表名、字段名是否存在、* 替换成所有字段

优化器会根据CBO模型选择查询成本最小的执行计划,比如选择不同索引、访问方式或者 Join 顺序

执行器根据执行计划执行sql语句 通过存储引擎提供的接口读写记录并把结果返回给客户端

存储引擎!

MySQL 存储引擎采用的是插件式架构,支持多种的数据库表设置不同的存储引擎以适应不同场景的需要。存储引擎是基于表的,而不是数据库

MyISAM(有较低的存储空间和内存消耗,适用于大量读操作的场景)、MEMORY(数据全在内存,读写快,安全性低,对表大小有要求)

与MyISAM的三大区别,也是InnoDB的三大特点:

常用InnoDB的原因是支持事务,且最小锁的粒度是行级锁,redolog可以崩溃恢复

InnoDB存储引擎架构

逻辑存储结构

表空间 段 区 默认16KB数据页 row

InnoDB 的数据是按「数据页」为单位来读写的B+ 树的每一个节点,在物理层面上严格对应着一个 16KB 的页,如果是非叶子节点仅用来存放目录项作为索引

页内部查找

数据页内包含用户记录,每个记录之间用单向链表的方式组织起来,为了加快在数据页内高效查询记录,设计了一个页目录,页目录存储各个槽(分组),且主键值是有序的,于是可以通过二分查找法的方式进行检索从而提高效率。

数据页中有一个页目录,起到对用户记录的索引作用,

利用页目录查找,本将长的单链表O ( N ) O(N)O(N)遍历,转化为了数组二分查找O ( l o g M ) O(\\log M)O(logM)M是slot的数量+ 极短链表遍历O ( 1 ) O(1)O(1)。

内存架构 Buffer Pool

Buffer Pool Change buffer 专门用来缓存对“非唯一二级索引页”的修改操作 针对非唯一二级索引页(允许索引列存在重复的值) 如果DML数据不在buffer pool中 不会直接操作磁盘 先将变更存到change buffer中 在未来数据被读取时再合并 合并后再落盘

对于聚簇索引 / 唯一二级索引 当数据页不在Buffer Pool中必须立即从磁盘加载 因为 要保证数据的绝对一致性和唯一性

log buffer 自适应哈希索引

后台线程架构

负责将数据从内存写入磁盘(落盘)

索引!

是什么和优缺点

索引本质上是mysql为了减少磁盘 I/O 次数而设计的数据结构,其本质可以看成是一种排序好的数据结构; 在 InnoDB 中主要采用 B+ 树实现

优点:

  1. 加速查询 减少io
  2. 保证数据唯一性
  3. 加速排序和分组

缺点:

  1. 维护成本:创建索引本身需要时间,特别是对大表操作时。尤其增、删、改DML语句需要维护索引
  2. 占用存储空间
  3. 误用或失效

索引越多越好吗

不是越多越好,不论是从空间、时间、优化器选择、维护成本来说都不是越多越好。

建议 单张表索引不超过 5 个!

什么字段适用索引?

索引结构

创建的主键索引和二级索引默认使用的是 B+Tree

红黑树 ,AVL,bst都是二叉的,随着插入的元素增多,而导致树的高度变高,导致磁盘 I/O 次数过多

B+Tree vs B Tree
  1. 首先 B+树非叶子节点不存储数据,仅放索引(索引键和页指针),因此读取同样一数据页(16KB)的情况下,相比B树能路由的节点多很多,就使得整个树更加矮胖,减少了磁盘IO查询次数
  2. 其次 B+树的叶子节点用双向链表连接,有利于范围查询,顺序 I/O;而b树则需要从根节点遍历,就涉及到多次IO查询(当然你可以中序遍历线索化b树,但是工程上非常麻烦,B+ 树之所以能做到,是因为它用“牺牲非叶子节点存储冗余索引键”的代价,换来了“底层叶子节点全量数据集合”的物理特性。)
  3. 最后 B+树的冗余节点(所有非叶子节点都是冗余索引)的特性使得增删操作对 树结构的变化调整没有b树那么频繁

为什么树高和io次数成正比: 索引存放在磁盘上,操作系统在读取数据时按“页”为单位加载,树的每一层通常对应一次随机磁盘 I/O

索引分类

按照「数据结构」维度划分:B+、hash(innoDB没有)、fulltext 按「物理存储」分类:聚簇索引(主键索引)、二级索引(非聚簇索引)。 按照「字段特性」:主键索引、唯一索引(UNIQUE,能让优化器更早停止查找)、普通索引、前缀索引 按「字段个数」分类:单列索引、联合索引。

创建的聚簇索引和二级索引默认使用的是 B+Tree 索引。

底层存储方式角度划分:
聚簇索引/主键索引:

索引结构和数据一起存放的索引,必须有且仅有一个

存在性保证:如果有主键则主键索引是聚簇索引,不存在主键就用第一个不包含 NULL 值唯一列作为索引,还没有的话InnoDB自动创建一个隐藏的6Byte 的自增主键rowid

二级索引/非聚簇索引 :

叶子节点内容不是整行数据,而是主键值故需要回表查询 除非它是覆盖索引

更新代价比聚簇索引要小

覆盖索引和回表

索引结构已经包含sql所有需要查询的字段的值,无需回表

按照「字段个数」分类:
单列索引
联合索引

多个字段上建立的索引,就是联合索引

最左前缀匹配原则

在使用联合索引时,查询必须从从左到右,不能跳过中间的列,否则索引将部分失效(失效指的是失去了对后面的列二分查找的能力) 联合索引(a,b,c)按照a → b → c的顺序匹配,不能跳过中间列。

例如:

WHEREa=1ANDc=3

a可以用于索引定位,b缺失后,c无法继续用于索引查找;MySQL 5.6+ 可以通过ICP 索引下推在存储引擎层过滤c,减少回表。

WHERE 条件书写顺序通常不影响,优化器会自动调整。

最左匹配与范围查询截断

口诀:等值条件放前面,范围条件放后面。

范围截断导致失效原因:

利用索引的前提是索引里的 key 是有序的!而联合索引是a 全局有序,a 相同时 b 才有序,a、b 相同时 c 才有序。** ^5b7975

索引下推:

存储引擎在二级索引遍历过程中,执行部分WHERE字句的判断条件,提前过滤掉不满足条件的记录,减少去主键树查完整数据的回表

出现了 Extra 为Using index condition就是用到了

如果没有覆盖索引情况 联合索引和单列索引哪个性能好?

联合索引优势:在多条件查询(如WHERE a AND b)中更好,能一次性过滤多个列,减少回表行数和I/O开销;也适合范围查询或排序。单列索引优势:在单一条件查询中性能相当或略优,更灵活,占用空间少;但多条件时可能需索引合并,效率较低。多条件查询优先考虑联合索引,核心目的是减少扫描数据量和回表次数。

前缀索引

前缀索引是指对字符类型字段的前几个字符建立的索引 仅限于字符串类型,较普通索引会占用更小的空间

索引失效

“失效”主要指失去了利用 B+ 树有序性继续缩小扫描范围的能力

  1. 联合索引不满足最左前缀匹配原则

    ^5b7975

  2. 在索引列上进行表达式计算、函数、类型转换等操作

    索引保存的是索引字段的原始值,只能通过把索引字段的取值都取出来。但是新版本可以生成函数处理后的索引

    索引隐式类型转换——字符串不加引号 MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较 索引列字段被Mysql cast了 回到情况2

  3. 索引头部模糊匹配

    左或者左右模糊匹配的时候,也就是like %xx或者like %xx%这两种方式失效 原因:B+ 树是按照「索引值」有序排列存储的,只能根据前缀进行比较,

  4. 查询条件中使用 OR,且 OR 的前后条件中有一个列没有索引,涉及的索引都不会被使用到

    全表扫描了 用啥索引 解决方法可以是创建另一条件的索引 然后MySQL会对结果集进行合并

  5. mysql优化器评估使用索引比全盘扫描慢

左、变、模、或、估

索引设计原则

防止索引失效

选择合适的列:不为 NULL 的字段区分度高的列 where查的多的数据 被频繁更新的字段应该慎重建立索引

利用覆盖索引,设置合适的联合索引

控制索引数量

事务 !

事务是逻辑上的一组操作,要么都执行,要么都不执行

事务有哪些特性? ACID

MySQL 默认是隐式提交当出现START TRANSACTION语句时,会关闭隐式提交; 当COMMIT或ROLLBACK语句执行后,事务会自动关闭,重新恢复隐式提交。

通过set autocommit=0可以取消自动提交;autocommit标记是针对每个连接而不是针对服务器的。

事务ACID原理 实现机制

事务的 ACID 分别靠什么机制保证? 事务保证

并发事务和隔离级别 (隔离性相关)

并发事务带来的问题:

  1. 脏读:读到其他事务未提交的数据;
  2. 不可重复读:前后读取的数据不一致;
  3. 幻读:前后读取的记录数量不一致。

为解决并发问题,SQL 提出了四种隔离级别,隔离级别越高,意味着性能越差

- **读未提交(_read uncommitted_)**,指一个事务还没提交时,它做的变更就能被其他事务看到; - **读提交(_read committed_)**,指一个事务提交之后,它做的变更才能被其他事务看到; - **可重复读(_repeatable read_)**,一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,**MySQL InnoDB 引擎的默认隔离级别**; - **串行化(_serializable_ )**;会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;

读已提交 RC 和可重复读 RR 的区别? 定义的不同 实现方式不同,即创建 Read View 的时机不同

Mysql 默认隔离级别是 可重复读RR 但是它很大程度上避免幻读现象

四种隔离级别分别如何实现的 ?

1, “读未提交” 级别的事务 直接读取最新的数据就好了 2, “读已提交和可重复读(RR) ” 级别的事务 通过Read View 来实现的,它们的区别在于创建 Read View 的时机不同* 「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View,而「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View 3, “串行化” 级别的事务 加读写行锁的方式来避免并行访问

MVCC/多版本并发控制

MVCC 是一种并发控制的方法,它通过undo log维护数据的版本链,并通过一套可见性规则也就是Read View让事务只能读到它应该看到的那个历史版本

MVCC 和 ReadView 的关系? 简单来说,MVCC是 InnoDB 实现高并发无锁读取的宏观机制,而 ReadView是这套机制中用来进行‘可见性判断’的核心数据结构。它们是整体与局部的关系,ReadView 配合隐藏字段和 Undo Log 版本链,共同构成了 MVCC 的完整实现。

MVCC实现原理:

锁也能实现隔离 但为了极致的并发性能把 MVCC 作为其并发控制的核心机制 在需要“当前读”等强一致性场景下,依然会使用锁作为 MVCC 的补充

RR隔离级别下,readview和锁机制如何减少幻读的发生的?MySQL 如何解决幻读问题?

第一个,快照读(普通的 SELECT):依靠 MVCC 和 ReadView事务在首次查询时生成全局ReadView,并在整个事务周期内复用。如果有其他事务插入了新数据,因为readview会屏蔽其他事务的改动,当前事务也只能看到历史快照,所以新数据不可见,避免了幻读。

第二个点,当前读(所有涉及数据修改的操作 DML,以及显式加锁的查询):依靠临键锁(Next-Key Lock)物理阻塞。执行当前读会在查询范围内加上临键锁,不仅锁住存在的记录,还会锁住记录之间的间隙。这会直接阻塞其他事务在这个范围内的INSERT操作,从物理上阻止了幻影行的产生。也会阻止UPDATE和DELETE(防并发篡改)

例外,快照读与当前读混用会导致幻读。如果事务 A 先执行快照读,此时事务 B 插入了一条新记录并提交。接着事务 A 执行了一次范围UPDATE(当前读),恰好把事务 B 刚插入的记录也更新了。这会导致该记录的隐藏字段的事务 ID 变成事务 A 自己。当事务 A 再次执行快照读时,根据 MVCC‘自己修改的数据对自身可见’的规则,这条原本看不见的幻影行就会出现,真正的幻读就发生了。”

每行record的两个隐藏字段:

日志体系架构

错误日志 查询日志 慢查询日志

redo log / undo log / binlog 区别? redo log 在 InnoDB 引擎层,采用循环写的 WAL 机制,保证崩溃恢复时的持久性;undo log 记录反向操作,支持事务回滚和MVCC实现;binlog 在 Server 层,追加写入,用于主从复制和数据备份。三者通过 “两阶段提交” 保证一致性,是 MySQL 高可用架构的基石。

undo log / 回滚日志

功能:事务回滚 和 MVCC

undo log 在 Insert 和 Update 时分别记什么?

redo log

为什么不直接刷盘?/ 有了undo lpg 为什么要有redo log? 问题:bufferpool 提高读写效率,但是他是基于内存的,不可靠。一个事务提交成功,但脏页未正常落盘,比如断电重启,持久性被打破,故有 redo log机制

redo log 的作用:

怎么保持持久性的: WAL(write-ahead-logging)机制MySQL 的写操作并不是立刻写到磁盘上,而是先写redo日志,然后在合适的时间再写到磁盘上。

二进制日志(BINLOG)

记录了所有的 DDL(数据定义语言)语句和 DML(数据操纵语言)语句,但不包括数据查询(SELECT、SHOW)语句。 作用:数据备份和主从复制

binlog 有 3 种格式类型,分别是 STATEMENT、ROW(MySQL 5.7.7 及之后的默认格式)、MIXED

redo log 和 binlog 有什么区别?为什么不能替代redo log做崩溃恢复? binlog是 server 层的日志,没办法记录哪些脏页还没有刷盘,redolog 是存储引擎层的日志,可以记录哪些脏页还没有刷盘,这样崩溃恢复的时候,就能恢复那些还没有被刷盘的脏页数据。

redo log 和 binlog 的 “两阶段提交” 机制

SQL优化!

核心思路:先定位 再分析 后优化

MySQL中如何定位慢查询/如果SQL语句执行很慢,如何分析和优化?

EXPLAIN 怎么分析执行计划、一个事务中有多条慢语句怎么排查?

定位慢查询:

首先得开启慢查询日志。而且要设置合适的long_query_time阈值,默认 10 秒,我们生产环境设置的2秒 然后可以使用mysqldumpslow工具分析慢查询日志,按执行时间、执行次数排序,快速锁定最需要优化的 SQL

必要时,我也会结合SHOW PROFILE(虽然已弃用)查看这条SQL在各个执行阶段的具体耗时分布

分析:

拿到具体的慢SQL后,我会使用EXPLAIN关键字来深入分析它的执行计划。在这里,我主要会盯住四个核心字段:

优化

索引优化其实就是设计合适的索引

SQL语句优化

检查锁竞争:使用SHOW PROCESSLIST查看是否有锁等待。

架构层次优化

深分页优化

深度分页问题:当使用LIMIT offset, size且offset值很大时, MySQL 需要扫描回表offset + size条记录后丢弃前面的offset条,性能很差 优化核心是 **“用索引直接定位,避免扫描后丢弃”

“既然前面的数据反正要丢弃,为什么不直接在索引里跳过?” 事实上,MySQL 不能“直接计数然后跳过”,主要是由于MySQL 的架构分层以及InnoDB 的MVCC机制共同决定的

解决方案:
  1. 子查询先查 id:覆盖索引省回表
  2. 游标记住 last_id:直接定位无扫描
  3. 业务限制最大页:性价比高最实用

追问一:游标分页有什么缺点?什么场景不适合用? - 答:不支持跳页、需要前端保存游标状态、ID 不连续时可能漏数据。电商搜索结果页适合,但后台管理系统需要跳页就不适合。追问二:如果排序字段不是主键 ID,而是create_time,怎么优化? - 答:可以用联合索引(create_time, id),子查询改为SELECT id FROM orders ORDER BY create_time, id LIMIT offset, size,游标分页改为WHERE (create_time, id) > (last_time, last_id)。追问三:为什么子查询只查id就能提升性能? - 答:因为只查主键/索引列时,MySQL 可以使用"覆盖索引",直接在索引树上获取数据,不需要回表去聚簇索引查完整记录。

比如只看状态正常的帖子SELECT id FROM table WHERE status = 1 ORDER BY create_time LIMIT 100000, 10

SQL语句优化

指定索引useselect * 容易出现回表查询

插入数据批量insert 手动提交事务 主键顺序插入性能高于乱序 大批量插入 用load主键优化表数据在叶子节点链表来看,是根据主键顺序存放的 即 叶子节点是有序的 故顺序插入显著减少页分裂、页合并(删多了 会合并比较”空“的页)

子查询之所以快,是因为:

  1. 大部分情况下,利用PAGE_MAX_TRX_ID优化,直接在二级索引里就完成了可见性判断,彻底免除了回表。
  2. 即便需要判断可见性,由于二级索引体积小,读取它的成本比读取挂载了大量字段的聚簇索引要低得多。

limit可以用where id>=and id<=来替代,这样非常快。之所以不这样是因为某些id可能被删掉了,不连续。但是业务中要使用分页查询的数据都要保证id连续完整。count 优化update优化InnoDB行锁针对索引而不是记录 并且该索引不能失效 否则行锁升级为表锁 影响并发

锁!

全局锁

全库的逻辑备份使用 导致业务停滞

避免加全局锁方案 备份时使用--single-transaction参数一致性数据备份

表级锁

意向锁是为了提高加表锁的效率。如果没有意向锁,想加表锁时需要逐行检查是否有行锁,开销巨大。有了意向锁,只需检查表级别的意向锁即可,大大降低了检测开销。

行级锁!

MyISAM 引擎并不支持行级锁,这是innoDB的特点之一,也是优势所在(粒度小 提高了并发度)

锁相容矩阵:

当读操作是锁定读时,才会受X锁影响,普通读(非锁定读)是不会对记录加锁的,因为它属于快照读,除非在串行化隔离级别下

什么时候加 X 锁(Exclusive Lock,排他锁)?

  1. SELECT … FOR UPDATE应用场景:高并发下的“悲观锁”设计。例如电商扣减库存,先用FOR UPDATE锁住该商品行,防止“超卖”。
  2. 数据变更语句(DML)自动加锁

不通过索引改数据会导致行锁升级为表锁: InnoDB 的行锁是加在索引上的。如果一条UPDATE或DELETE语句的WHERE条件没有走索引,MySQL 就无法通过索引快速定位记录。为了保证事务隔离性,系统不得不对聚簇索引(主键索引)中的所有记录以及它们之间的间隙全部加上锁。

当事务执行 commit 后,事务过程中生成的锁都会被释放

什么是死锁?怎么解决/预防?

数据库死锁是多个事务在执行过程中,因竞争锁资源而造成相互等待的现象,除非外力干预打破这种循环等待,否则的话这些事务都无法继续执行

我举个例子:“事务 A 获取了记录 1 的 X 锁,想要去更新记录 2; 与此同时,事务 B 获取了记录 2 的 X 锁,想要去更新记录 1。 这个时候,事务 A 等待 B 释放记录 2 的锁,事务 B 等待 A 释放记录 1 的锁,形成闭环,死锁就产生了。”

InnoDB 是如何处理死锁的? 主动检测死锁,事务等待图是否存在环,将主动回滚 undo 量最小的那个事务 超时机制(被动兜底)默认50s

解决方案:

  1. 统一加锁顺序,比如在批量处理多个用户的资金转账时,强制要求代码先按用户 ID 进行排序,然后再依次执行UPDATE,这样就能彻底避免交叉等待。
  2. 小事务减锁持有时间,
  3. 加索引减少锁范围(尽量让查询条件走唯一索引或主键索引,确保加的是记录锁),
  4. 开检测做兜底

主从复制

作用

数据备份 失败迁移 读写分离 降低单库读写压力

原理

线程角度:

为什么需要 Relay Log 而不能直接从 Master 的 Binlog 执行

MySQL 读写分离

读写分离的核心目标是分摊主库的读压力,但引入它的同时,必须解决主从复制延迟带来的数据一致性问题(C)。

主从复制的三种底层机制(性能与一致性的权衡)
如何解决主从延迟导致的“写后即读”数据不一致?

(面试官必问:刚写完主库,立马去读从库,读不到怎么办?)

我通常会根据业务场景的容忍度采用三种解法:

读写分离下的“事务”如何平滑处理?

核心原则:只要事务中包含任何写操作(INSERT/UPDATE/DELETE),为了保证 ACID 原子性,该事务内的所有读写请求,必须死死绑定在主库执行,绝不能跨库拆分。

工程落地方式(结合 Java 栈):

MySQL 磁盘 I/O 很高,有什么优化的方法?

大表问题

“大表”的本质与性能拐点

调优路径(遵循 KISS 原则,由轻到重)

1. 索引优化与缓存(千万级以内 / 单纯查询慢)

为什么几十亿数据有索引依然慢?

  1. B+ 树长高(层级跃升):树结构从常规的 3 层膨胀至 4~5 层,拉长搜索路径。
  2. 随机 I/O 剧增:树每增高 1 层,最坏情况多 1 次磁盘寻址。若走二级索引,叠加回表操作,I/O 次数呈指数级放大。
  3. 内存命中率雪崩(致命根因):几十上百 GB 的巨型索引无法完整驻留内存,引发高频的Cache Miss(缺页中断)。查询被迫不断从磁盘加载数据页,彻底拖垮整体系统的 QPS 与响应延迟。

分库分表

分库分表是应对大数据量、高并发场景的数据库拆分策略:

垂直分库:按业务模块拆分到不同数据库(用户库、订单库)。 垂直分表:将大表按列拆分成多个小表(将长文本字段拆分出去)。 水平分库:将同一张表的数据按规则(取模、范围)分布到多个数据库实例。 水平分表:将一张表的数据按规则拆分到多个结构相同的表中(可放在同一库或不同库)。

中间件怎么做的

以客户端模式(如 ShardingSphere-JDBC)为例,核心是在应用层通过 AOP 机制或 ORM 框架的插件(比如 MyBatis 的 Interceptor)来劫持 SQL 执行流程

分库分表会带来什么问题呢

分库分表后,数据怎么迁移呢?

  1. 停机迁移
  2. 双写方案
  3. 数据库同步工具 Canal 做增量数据迁移(原理依赖于binlog)

原文地址:MySQL

本文持续更新,最新内容及相关资料请访问原文。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进