底层原理
1. 基础架构
MySQL可以基本划分为Server层 和存储引擎层两部分。
服务层分别有连接器、分析器、优化器、执行器。

2. 存储引擎及其底层
1. InnoDB(默认引擎)
底层实现 :B+树索引(非叶子节点只存储指针,叶节点由双向链表链接,可范围查找)
支持事务,外键,MVCC ;行级锁。
2. Memory(内存引擎)
底层实现 :哈希索引 (默认)或B树索引。(数据完全在内存)
3. MyISAM
底层实现 :B树
不支持事务、外键;表级锁。
B+树相比B树的优势?
技术对比:
维度 | B+树 | B树 |
---|---|---|
数据存储 | 仅叶子节点存数据 | 所有节点存数据 |
查询性能 | 稳定O(log n) | 最好O(1)最差O(log n) |
范围查询 | 叶子节点链表支持高效遍历 | 需中序遍历 |
优化
1. 慢查询
造成原因:聚合查询 ,多表查询 ,表数据量过大查询,深度分页查询
定位方案:
- 调试期MySQL开启慢日志查询,设置为 2 秒 (slow_query_log = 1)
SQL执行计划
(执行慢,如何分析?)
可以采用 EXPLAIN 或者 DESC 命令获取如何执行SELECT语句的信息。
展示的参数:
- possible_keys:当前sql可能用到的索引。
- key:当前sql实际命中的索引。
- key_len:索引占用的大小。
- type:性能排序为const(主键) > eq_ref(唯一索引) > ref(索引) > range > index(索引树)。
- extra:优化建议。
2. 索引
索引是一种有序数据结构。
底层: MySQL存储引擎的底层数据结构B+树满足索引的特定查找算法,提高查找效率。
1. 聚簇索引和非聚簇索引
分类 | 含义 | 特点 |
---|---|---|
聚簇索引 | 数据与索引一起存储,保存 了行数据 | 必须有且仅有一个 |
二级索引(非聚簇) | 数据和索引分开存储,保存 对应主键 | 可存在多个 |
聚簇索引选取规则:
- 存在主键,则主键索引就是聚簇。
- 没有主键,第一个唯一(UNIQUE)索引就是聚簇。
- 没有主键和唯一索引,则InnoDB生成一个 rowid 作为隐藏聚簇。
回表查询
通过二级索引 找到对应的主键值,再到聚簇索引中拿到整行数据。
2. 覆盖索引
定义:查询使用索引,需要返回的列在该索引中能全找到。(回表查询 的就不是覆盖索引,避免select *)
超大分页如何处理?
- 问题:大数据量,limit分页查询,需要对数据排序,效率低。
- 解决方案:覆盖索引 + 子查询。(走覆盖索引,获取主键,再到原来的表做关联查询。)
3. 索引创建原则
- 尽量使用联合索引
- 单表超10w数据,查询频繁
- 针对查询条件、排序、分组的字段建立索引
- 控制索引的数量
最左前缀原则
带头大哥不能死,中间兄弟不能断。
例如:在姓名 列和电话列设置联合索引。
- 使用两个字段,可以用到联合索引(注:两个字段的顺序颠倒并不影响,因为全值匹配时mysql会优化字段顺序)
- 使用左边的name字段,可以用到联合索引
- 使用右边的phone字段,无法使用索引,全表扫描
4. 索引失效场景
-
索引列运算 (函数、表达式、计算)。因为当前值改变后就无法与索引存的值匹配上。
SELECT * FROM user_innodb where left(name, 3)='张三';-- left函数返回具有指定长度的字符串的左边部分
-
范围查询 (!=,<=>,in)会导致右边列失效。因为二叉树的查找是 = 查找,若是一个范围的话无法继续下探。
SELECT * FROM user_innodb where name='张三' and age > 22;
-
like以%开头 ('%abc...'),mysql索引失效会变成全表扫描操作。因为无法判断%代表多少字符。
SELECT * FROM user_innodb where name like '%三';
-
字符串不加' '(类型转换) 索引失效。因为会出现隐式转换,相当于给索引列做了操作。
SELECT * FROM user_innodb where name = 007;-- "007"从字符串变成了数字007
-
用or,用它连接时很多情况下索引会失效。
SELECT * FROM user_innodb where name = '张三' or name = '李四';
-
is null,is not null 无法使用索引。
SELECT * FROM user_innodb where name is null;
-
联合索引违反最左前缀原则。
5. SQL优化有哪些?
- 表的设计优化 (数据类型的选择)
- 索引优化 (索引创建原则)
- SQL语句优化 (避免select *、避免where子句有表达式、使用innerjoin ......)
- 主从复制、读写分离
- 分库分表
事务
1. 事务特性(ACID)
- 原子性(Atomicity):事务是不可分割的最小操作单元
- 一致性(Consistency):事务完成时,所有数据 必须保持一致
- 隔离性(Isolation):保证事务不受 外部并发 操作影响
- 持久性(Durability):一旦提交或回滚,对数据库的改变 是永久的
2. 并发事务问题及隔离级别
- 并发事务问题:脏读、不可重复读、幻读
问题 | 现象描述 | 示例场景 |
---|---|---|
脏读 | 事务A读取到事务B未提交的修改 | 事务B修改余额未提交,事务A看到100→200,B回滚后实际余额仍为100 |
不可重复读 | 事务A内两次读取同一数据 ,期间事务B修改了该数据并提交,导致两次结果不一致 | 事务A查询库存为10,事务B下单扣减为8并提交,事务A再次查询变为8 |
幻读 | 事务A按条件查询范围数据,期间事务B插入/删除符合该条件的记录,导致结果集变化 | 事务A查询年龄>30的用户(初始2条),事务B插入新用户后,事务A再查得到3条 |
- 隔离级别:读未提交、读已提交、可重复读、串行化
隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现机制 | 性能 |
---|---|---|---|---|---|
读未提交 | ❌ 可能 | ❌ 可能 | ❌ 可能 | 无锁,直接读最新数据 | 最高 |
读已提交 | ✅ 避免 | ❌ 可能 | ❌ 可能 | MVCC快照读(每次查询生成新ReadView) | 高 |
可重复读(RR) | ✅ 避免 | ✅ 避免 | ❌ 可能* | MVCC快照读(事务首次 查询生成ReadView) | 中等 |
串行化 | ✅ 避免 | ✅ 避免 | ✅ 避免 | 全表锁(读加共享锁,写加排他锁) | 最低 |
注:InnoDB在RR级别通过间隙锁可避免幻读,但标准SQL规范中RR允许幻读
3. undo log 和 redo log的区别
维度 | undo log | redo log |
---|---|---|
日志类型 | 逻辑日志(记录反向SQL) | 物理日志(记录页修改) |
主要作用 | 事务回滚 + MVCC多版本并发控制 | 崩溃恢复 + 事务持久性 |
写入时机 | 事务开始前 | 事务执行过程中 |
解决问题 | 保证事务原子性,一致性 | 保证事务持久性 |
关联事务 | 每个事务独立记录 | 所有事务混合记录 |
4. MVCC多版本并发控制
本质是通过多版本并发控制避免读写冲突 ,主要依赖于隐藏字段、undo log日志、ReadView。
组件 | 作用 | 物理表现 |
---|---|---|
隐藏字段 | 记录行数据的事务信息 | DB_ TRX_ID、DB_ ROLL_PTR、DB_ROW_ID |
Undo Log | 存储数据的历史版本,形成版本链 | 回滚段(Rollback Segments) |
ReadView | 事务执行时生成的"快照",决定哪些版本对当前事务可见 | m_ids 、min_trx_id 、max_trx_id |
隐藏字段
- 修改它的事务ID (
DB_
TRX_ID):每进行一次事务操作,就会自增1。 - 指向上一版本的指针 (
DB_
ROLL_PTR):结合undolog进行回滚。
ReadView
**通过readview我们才知道自己能够读取哪个版本。**在一个readview快照中包括四个字段:

- m_ids:活跃的事务就是指还没有commit的事务。
- max_trx_id :预分配事务id。例如m_ids中的事务id为(1,2,3),那么下一个应该分配的事务id,即max_trx_id就是4。
- creator_trx_id:执行select读这个操作的事务的id。
readview如何判断版本链中的哪个版本可用呢?

(1)如果要读取的事务id等于进行读操作的事务id,说明是我读取我自己创建的记录,可读取。
(2)如果要读取的事务id小于最小的活跃事务id,说明要读取的事务已经提交,可读取。
(3)如果要读取的事务id大于max_trx_id,说明该id已经不在该readview版本链中了,无法读取。
如何实现RC和RR的隔离级别
- RC 的隔离级别下,每个快照读 都会生成 并获取最新的readview。
- RR 的隔离级别下,只有在同一个事务 的第一个快照读 才会创建 readview,之后的每次快照读都使用的同一个readview ,所以每次的查询结果都是一样的。
5. 主从同步
MySQL主从复制的核心就是二进制日志(BinLog)。
- Master 主库事务提交,数据变更记录在BinLog。
- slave 从库读取BinLog,写入 到从库的中继日志Relay Log。
- slave重做relay log的事务。
6.分库分表
- 垂直分库:以表 为依据,根据业务将不同表拆分到不同库。
- 垂直分表:以字段 为依据,根据字段属性将不同字段拆分到不同表。
- 水平分库:将一个库的数据拆分到多个库。
- 水平分表:将一个表的数据拆分到多个表。(可以在同一个库)
MySQL锁
一、死锁
两个事务对一张表进行操作
sql
session1 session2
begin; begin;
select * from test where id = 3 for update; select * from test where id = 4 for update;
insert into test(id, name) values(3, "test1");
insert into test(id, name) values(4, "test2");
锁等待中
锁等待解除
死锁,session 2的事务被回滚
上面两个并发事务一定会发生死锁(只有RR和Serializable 隔离级别下才会有间隙锁/临键锁,而这是导致死锁的根本原因)。
select ... for update虽然可以用于解决数据库的并发操作,但在实际项目中却不建议使用,原因是当查询 条件对应的记录不存在 时,很容易造成死锁。
二、锁的分类
1.区间划分
1、间隙锁(Gap Locks)
实例 : (3, 4)
间隙锁是开区间 的,是一个在索引记录之间的间隙上的锁。
作用 :保证某个间隙内的数据 在锁定情况下不发生 任何变化 。
当使用唯一索引来搜索唯一行的语句时,不需要间隙锁定。如下面语句的id列有唯一索引,此时只会对id值为10的行使用记录锁。
select * from t where id = 10 for update;// 注意:普通查询是快照读,不需要加锁
- 如果,上面语句中id列没有建立索引 或者是非唯一索引 时,则语句会产生间隙锁。
- 如果,搜索条件里有多个查询条件 (即使每个列都有唯一索引),也是会有间隙锁的。
根据检索条件向下寻找最靠近检索条件的记录值A作为左区间,向上寻找最靠近检索条件的记录值B作为右区间,即锁定的间隙为(A,B)
2、临键锁(Next-key Locks)
临键锁是行锁+间隙锁 ,即临键锁是是一个左开右闭的区间,比如(- ∞, 1 ] |(1, 3 ] |(3, 4 ] | (4, + ∞)。
2.粒度划分
1、表级锁
直接给整个表添加锁:
select * from student where name = 'tom' for update
InnoDB在使用过程中只要不通过索引检索 数据时,全部是表锁 。
开销小,加锁快;不会 出现死锁 ;锁定粒度大,发生锁冲突的概率最高,并发度最低
MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此用户一般不需要直接用LOCK TABLE命令给MyISAM表显式加锁。
2、行级锁
InnoDB中给指定的行添加锁:
select * from student where id > 10 for update
InnoDB行锁是通过给索引 上的索引项加锁来实现的,这一点,MySQL于Oracle不同,后者是通过在数据块中对相应的数据行加锁来实现的,InnoDB只有通过索引条件检索数据,InnoDB才使用行级锁
行锁的劣势:开销大;加锁慢;会 出现死锁
行锁的优势:锁的粒度小,发生锁冲突的概率低;处理并发的能力强
3、页级锁
页级锁是 MySQL 中比较独特的一种锁定级别,在其他数据库管理软件中并不常见。
页级锁和行级锁一样,会 发生死锁 。页级锁主要应用于 BDB 存储引擎。
3.级别划分
1、共享锁(即S锁)
共享锁(S):又称读锁 ,自己和其他事务都只能读不能写。
2、排它锁 / 独占锁(即X锁)
排它锁(X):又称写锁,自己可读可写,其他事务不能读写。
3、意向锁
事物B对一行数据使用行锁,当有另一个事物A对这个表使用了表锁,那么这个行锁就会升级为表锁。
当一个事务在需要获取资源的锁定时,如果该资源已经被排他锁占用,则数据库会自动给该事务申请一个该表的意向锁。如果自己需要一个共享锁定,就申请一个意向共享锁。如果需要的是某行(或者某些行)的排他锁定,则申请一个意向排他锁。
4.使用方式划分
1、乐观锁
先进行业务操作,不到万不得已不去拿锁
2、悲观锁
先获取锁,再进行业务操作