目录
- 开头
- 一.索引
-
- [1.底层基础:磁盘 IO 与 Page 机制](#1.底层基础:磁盘 IO 与 Page 机制)
-
- [(1)buffer pool 缓冲池](#(1)buffer pool 缓冲池)
- [(2)Page 结构](#(2)Page 结构)
-
- [a. 单个 Page 的内部结构](#a. 单个 Page 的内部结构)
- [b.多 Page 组织:从线性链表到 B + 树](#b.多 Page 组织:从线性链表到 B + 树)
- [c.B + 树](#c.B + 树)
- [B + 树的核心优势](#B + 树的核心优势)
- [(3)聚簇索引 / 非聚簇索引](#(3)聚簇索引 / 非聚簇索引)
-
- [a.MyISAM 引擎:全非聚簇索引](#a.MyISAM 引擎:全非聚簇索引)
- [b. InnoDB 引擎:聚簇主键 + 非聚簇辅助索引](#b. InnoDB 引擎:聚簇主键 + 非聚簇辅助索引)
- 2.索引操作
- [二. 事务](#二. 事务)
-
- 1.事务的四大核心特性(ACID)
- 2.事务的提交模式与基础操作
- [3.事务隔离级别 ------ 隔离性的分级实现](#3.事务隔离级别 —— 隔离性的分级实现)
-
- [(1)Read Uncommitted :读未提交](#(1)Read Uncommitted :读未提交)
- [(2)Read Committed :读提交](#(2)Read Committed :读提交)
- [(3)Repeatable read:可重复读](#(3)Repeatable read:可重复读)
- (4)Serializable:串行化
- [4.多版本并发控制( MVCC )](#4.多版本并发控制( MVCC ))
-
- (1)3个行隐藏字段
- [(2)undo log日志](#(2)undo log日志)
- [(3)read view :读视图](#(3)read view :读视图)
-
- a.核心规则:数据可见性判断
- b.两种读取模式
- [c.RC 与 RR 隔离级别的本质区别](#c.RC 与 RR 隔离级别的本质区别)
- 三.视图
- 结尾
- 往期回顾
开头
Ok了,前面两期我们重点学习到了DDL、DML,今天这一期要来学习MySQL中的两个十分重要的板块------索引+事务,废话不多说,我们字节开始
一.索引
索引是典型的「空间换时间」优化方案,通过建立类似 "目录" 的数据结构,大幅减少磁盘 IO 次数,实现海量数据下的秒级查询。索引提升查询速度的代价,是写入(增删改)时需要额外维护索引结构,因此索引的价值在于优化高频查询场景
1.底层基础:磁盘 IO 与 Page 机制
磁盘是机械存储设备,依靠磁头移动寻址,随机 IO 的效率远低于连续 IO,数据库性能的核心瓶颈通常就是磁盘 IO 的次数
- 磁盘硬件的最小读写单位是扇区,传统大小为 512 字节
- 操作系统文件系统以4KB 数据块为单位和磁盘交互
- MySQL InnoDB 引擎和磁盘交互的基本单位是16KB 的 Page(数据页)
可通过命令验证 InnoDB 页大小:
SHOW GLOBAL STATUS LIKE 'innodb_page_size';
-- 返回值:16384 字节 = 16KB
(1)buffer pool 缓冲池
MySQL 在内存中开辟了一块大内存空间叫Buffer Pool,作为磁盘数据的缓存:
- 读写数据优先操作内存中的 Page,后续按策略刷新到磁盘
- 一次 IO 将整页 16KB 加载进内存,后续同页内的查询无需再访问磁盘
- 核心优化逻辑:减少 IO 次数,比提升单次 IO 数据量更重要

(2)Page 结构
a. 单个 Page 的内部结构
首先有一个问题:为什么MySQL与磁盘进行IO时,要以Page作为基本单位,为什么不用多少加载多少呢??
如果MySQL要查找id=2的记录,第一次加载id=1,第二次加载id=2,一次一条记录,那么就需要2次IO。如果要找id=5,那么就需要5次IO。
但,如果这5条(或者更多)都被保存在一个Page中(16KB,能保存很多记录),那么第一次IO查找id=2的时候,整个Page会被加载到MySQL的Buffer Pool中,这里完成了一次IO。但是往后如果在查找id=1,3,4,5等,完全不需要进行IO了,而是直接在内存中进行了。所以,就在单Page里面,大大减少了IO的次数
MySQL中要管理很多数据文件,目前我们可以将一个个独立的文件是由一个或者多个Page构成的
create table if not exists user (
id int primary key, --一定要添加主键哦,只有这样才会默认生成主键索引
age int not null,
name varchar(16) not null
);
--插入多条记录,注意,我们并没有按照主键的大小顺序插入哦
mysql> insert into user (id, age, name) values(3, 18, '杨过');
Query OK, 1 row affected (0.01 sec)
mysql> insert into user (id, age, name) values(4, 16, '小龙女');
Query OK, 1 row affected (0.00 sec)
mysql> insert into user (id, age, name) values(2, 26, '黄蓉');
Query OK, 1 row affected (0.01 sec)
mysql> insert into user (id, age, name) values(5, 36, '郭靖');
Query OK, 1 row affected (0.00 sec)
mysql> insert into user (id, age, name) values(1, 56, '欧阳锋');
Query OK, 1 row affected (0.00 sec)
如上图,我们向一张表中插入了5条数据,那么在Page结构中是什么样的呢??

一个 Page 不只是存储用户数据的内存块,还包含管理元数据和页内索引,整体分为三部分:
- 页头元数据
最核心的是两个双向链表指针,用于多页之间的组织:
page_prev:指向前一个相邻的 Pagepage_next:指向后一个相邻的 Page
所有 Page 通过这两个指针,在逻辑上组成双向链表,实现多页的有序串联。
- 用户数据区
存储一行一行的用户记录,有两个核心规则:
- 按主键升序排序:所有记录严格按照主键值从小到大排列,这是后续所有索引优化的基础。
- 记录间用链表串联:每条记录都有指针指向下一条记录,形成单向链表结构
- 页目录
页目录就是页内的 "小型索引",专门解决页内线性遍历效率低的问题。
- 存储内容:多个目录项,每个目录项 =
主键值 + 对应记录在页内的位置指针。 - 工作方式:目录项本身按主键有序排列,查找时先对页目录做二分查找,快速定位到记录所在的大致区间,再在小区间内遍历即可

b.多 Page 组织:从线性链表到 B + 树
当数据量超过单个 Page 的 16KB 容量时,MySQL 会开辟新的 Page 存储数据,所有 Page 通过page_prev/page_next组成双向有序链表,Page 之间按主键范围有序(前一页的最大主键 < 后一页的最小主键)

多页链表的性能问题
果只有线性链表结构,查找某条记录需要:
从第一个 Page 开始,逐个把 Page 加载到内存,遍历每个 Page 的页目录,直到找到目标记录。
本质是跨 Page 的线性查找,数据量越大,IO 次数越多。千万级数据可能需要上千次 IO,效率极低
解决方案:给 Page 加 "多层目录"
借鉴 "页目录加速页内查找" 的思路,给 Page 也加目录,形成多层级的目录结构:
- 目录页:不存储用户数据,只存储「下级 Page 的最小主键值 + 下级 Page 的物理指针」。
- 目录页本身也是 Page,大小同样是 16KB;因为只存键值 + 指针,单个目录页可以容纳几百上千个索引项

可是,我们每次检索数据的时候,该从哪里开始呢?虽然顶层的目录页少了,但是还要遍历啊?不用担
心,可以在加目录页

通过多层目录的叠加,最终形成B + 树结构,这是 InnoDB 索引的底层实现
c.B + 树

B + 树的核心优势
- 树高极低,IO 次数少:非叶子节点只存键值,单个节点能容纳大量索引项。千万级数据的 B + 树高度仅 3~4 层,意味着一次查询最多只需要 3~4 次磁盘 IO,相比线性遍历性能提升上百倍。
- 天然支持范围查询、排序:叶子节点按主键有序且通过链表串联,范围查询只需要找到起始位置,沿链表遍历即可,无需回溯。
- 查询性能稳定:任何主键查询都要从根走到叶子节点,IO 次数基本一致,性能波动小
模拟 MySQL 主键查询流程(聚簇索引)
以select * from user where id = 3;为例,完整查询流程如下:
- 定位根页: MySQL从数据字典中获取该表聚簇索引的根页物理地址
- 第一层:加载根目录页: 将根页加载到内存的buffer pool 中;在根页的索引项中二分查找,找到
id=3的所属的下级目录 - 中间层:逐层路由: 加载对应的下级目录页,继续二分查找索引项,定位更下层的页
- 叶子层:定位数据叶: 最终定位到对应叶子的Page数据页,加载到内存中
- 页内查找: 在Page数据页中的页目录中二分查找,找到
id=3的记录,取出返回给客户端
补充:Buffer Pool 缓存机制
加载过的 Page 会缓存在内存的 Buffer Pool 中。如果后续再次查询同一条或同页的数据,直接命中内存,无需磁盘 IO,这也是 MySQL 热点数据查询极快的重要原因
(3)聚簇索引 / 非聚簇索引
根据 "索引和数据是否存储在一起",索引分为聚簇索引和非聚簇索引两大类
- 聚簇索引:索引结构与用户数据存储在一起,B + 树的叶子节点直接存放完整的用户行数据。数据本身就是索引的一部分。
- 非聚簇索引:索引结构与用户数据分离,B + 树的叶子节点只存放 "数据的位置标识"(磁盘地址或主键),查完索引需要二次查找才能拿到完整数据
a.MyISAM 引擎:全非聚簇索引
MyISAM 的所有索引都是非聚簇索引,索引和数据完全分离
-
文件结构:
.MYI文件:存储索引B+树.MYD文件:存储真实用户数据
-
主键索引 :叶子节点存储对应记录的磁盘物理地址。
-
辅助索引(普通索引):结构和主键索引完全一致,叶子节点同样存储数据的磁盘地址。
-
查询流程:查索引 B + 树 → 得到磁盘地址 → 直接去磁盘对应位置读取数据。
-
特点:辅助索引无需回表到主键索引,直接通过地址读数据;但数据和索引分离,随机 IO 较多
b. InnoDB 引擎:聚簇主键 + 非聚簇辅助索引
InnoDB 是 MySQL 默认引擎,采用「一个聚簇索引 + 多个非聚簇辅助索引」的架构
(1)主键索引(聚簇索引)

- 叶子节点直接存储完整的用户行数据,整张表的数据就存放在主键 B + 树的叶子节点上。
- 一张表有且只有一个聚簇索引,优先级规则:
- 表有主键,主键就是聚簇索引
- 无主键,选第一个唯一非空索引作为聚簇索引
- 都没有,自动生成 6 字节的隐藏主键
DB_ROW_ID作为聚簇索引
- 优点:主键查询性能极高,一次 B + 树遍历就能拿到完整数据;范围查询效率高。
- 缺点:辅助索引查询需要回表;无序主键插入会导致页分裂,产生数据碎片
(2)辅助索引(非聚簇索引)
普通索引、唯一索引、前缀索引等都属于辅助索引,全部是非聚簇的

- 叶子节点不存储完整数据,只存储主键值。
- 回表查询 :通过辅助索引查询数据时,需要分两步:
- 遍历辅助索引 B + 树,找到对应的叶子节点,获取主键值
- 拿着主键值,去聚簇索引 B + 树中再查一次,拿到完整行数据
这个二次查询的过程就叫做回表
为什么辅助索引存主键,不存磁盘地址?
因为 InnoDB 是聚簇组织,数据页会因为插入、分裂发生位置移动,地址不稳定;而主键是永久不变的,存主键虽然多一次回表,但维护成本更低,数据一致性更好
2.索引操作
索引的四大类型:
- 主键索引(primary key)
- 特性:非空、唯一,一张表只能有一个,查询效率最高
- 适用:数据行的唯一标识,通常用自增 ID
- 唯一索引 (unique)
- 特性:值必须唯一,但允许为 NULL(多个 NULL 不算重复);一张表可以有多个
- 适用:业务上需要去重的字段,如手机号、邮箱、身份证号
- 普通索引(index)
- 特性:允许重复值,一张表可以有多个,开发中最常用
- 适用:频繁作为查询条件、但值不唯一的字段\
- 全文索引(fulltext)
- 特性:针对大文本字段的关键词检索
- 限制:默认仅 MyISAM 引擎支持,原生对中文支持差;中文全文检索通常使用 Elasticsearch 等专门引擎
a. 创建索引

b.查看索引


c.删除索引

二. 事务
为什么需要事务?------ 并发场景下的数据安全问题
在多客户端同时操作数据库的场景下,如果不对数据操作做任何控制,必然会出现数据一致性问题。最经典的就是火车票超卖问题:

要解决这类问题,数据操作必须满足四个核心要求:
- 一次买票的所有操作是一个整体,不能执行一半(原子性)
- 两个客户端的买票操作不能互相干扰(隔离性)
- 买票成功后数据要永久生效,不能因为崩溃丢失(持久性)
- 操作前后数据的状态始终是合法、一致的(一致性)
事务(Transaction)是一组逻辑上相互关联的 DML(增删改)语句集合,是数据库执行的最小逻辑单元 。
这一组语句对外表现为一个不可分割的整体:要么全部执行成功,要么全部执行失败并回滚到事务开始前的状态,不会出现执行了一半的中间状态。
在 MySQL 中,只有 InnoDB 存储引擎支持事务

1.事务的四大核心特性(ACID)
事务必须同时满足四大特性,简称 ACID:
- 原子性(Atomicity)
事务是最小的不可分割单元,事务内的所有操作要么全部完成,要么全部不完成,不会停在中间某个环节。
- 事务执行中如果发生错误 / 异常,会自动回滚(Rollback)到事务开始前的状态,就像这个事务从来没执行过一样
- 实现基础:
undo log(回滚日志)
- 一致性(Consistency)
事务执行前后,数据库始终处于数据完整性、业务规则都成立的一致状态。
- 比如转账场景:A 扣钱、B 加钱,事务结束后总金额必须不变
- 一致性是事务的最终目标,它由原子性、隔离性、持久性共同保障,同时也和业务逻辑强相关
- 隔离性(Isolation)
数据库支持多个事务并发执行,隔离性保证多个事务之间互不干扰,避免并发交叉执行导致的数据不一致。
- 隔离性分不同等级,等级越高,数据越安全,但并发性能越低
- 实现基础:锁机制 + MVCC(多版本并发控制)
- 持久性(Durability)
事务一旦提交(commit),对数据的修改就是永久生效的,哪怕后续系统崩溃、断电,数据也不会丢失。
2.事务的提交模式与基础操作
(1)两种提交模式
MySQL 的事务提交分为两种模式,由系统变量autocommit控制:

切换命令:
-- 关闭自动提交
SET AUTOCOMMIT = 0;
-- 开启自动提交
SET AUTOCOMMIT = 1;
-- 查看当前状态
show variables like 'autocommit';

(2)手动事务相关语法
-- 1. 开启事务(两种写法等价,推荐begin)
begin;
-- 或 start transaction;
-- 2. 执行业务SQL(增删改查)
-- 3. 创建保存点:可以回滚到指定中间位置
savepoint sp1;
-- 4. 回滚到保存点
rollback to sp1;
-- 5. 全部回滚:回到事务开启前的状态
rollback;
-- 6. 提交事务:所有修改永久生效
commit;

如上图,我们创建了一个表结构,下面会以它举例
为了方便演示,我们将隔离级别设置为读未提交


如上图,使用start transaction; 和 begin; 开启事务,因为MySQL的事务此时可以并发去访问数据区,我们现在也是用两台机器模拟一下
可以看到,向account 表中插入数据,另一个事务在 Read Uncommitted 的隔离级别下是可以读到的,做出的修改另外一个事务也是可以同步查看的

如上图,我们想要进行回滚也是Ok的

可是如果我们直接按下 Ctrl+\ 直接退出不保存,发现另一个事务现在什么都读不到了
看完以上的内容,你现在大概是云里雾里的,没关系,这只是我们为了引出隔离性,下面我们来正式学习下
3.事务隔离级别 ------ 隔离性的分级实现
数据库是多客户端并发访问的系统。如果多个事务同时读写同一张表、同一行数据,不加任何限制会出现脏读、不可重复读、幻读等数据异常。隔离级别就是用来定义「事务之间的可见性边界」,平衡数据安全与并发性能
三类并发异常

(1)Read Uncommitted :读未提交
级别定义
最低隔离级别,事务之间几乎没有隔离:所有事务都可以看到其他事务未提交的执行结果。
- 本质:写操作不加排他锁,读操作不加共享锁,直接读取最新版本
- 实际生产中几乎不会使用,仅用于理论学习
存在问题
脏读、不可重复读、幻读三类问题全部存在。
上面我们举得那个例子就是读未提交得典型例子
(2)Read Committed :读提交
级别定义
一个事务只能看到其他事务已经提交的修改,未提交的修改完全不可见。
- 是 Oracle、SQL Server 等大多数数据库的默认隔离级别
- 解决了脏读问题,但仍存在不可重复读和幻读
实现原理
每次执行SELECT快照读时,都会重新生成一个最新的Read View(读视图),所以每次都能看到最新提交的修改

如上图,我们现在将两个事务都设置成 Read Committed 隔离模式,以及 autocommit =0

如上图,read committed 隔离级别下,如果读没有提交,另外得事务就不会显示

当我们commit 提交后,在另一个事务中select 就可以查询到了,但是现在查得表中得数据并没有修改,这就是不可重复读问题

(3)Repeatable read:可重复读
级别定义
MySQL 默认隔离级别,保证同一个事务内,多次快照读取同一行数据,结果始终一致
- 解决了脏读、不可重复读问题
- 通过Next-Key 锁(间隙锁 + 行锁) 基本解决了幻读问题
实现原理
事务中第一次执行 SELECT 快照读时,生成一个 Read View,后续所有快照读都复用这个 Read View,所以看不到之后提交的修改。

现在我们将隔离级别设置为repeatable read 可重复读

如果我们没有 commit 提交,另一个事务就查询不到,后续我们更新或者回滚,都不会影响另一个事务查询结果------可重复读
(4)Serializable:串行化
级别定义
最高隔离级别,强制所有事务串行执行,读写操作全部加锁,事务之间完全不会交叉。
- 彻底解决脏读、不可重复读、幻读所有问题
- 并发性能极差,容易出现锁等待、超时、死锁,生产环境几乎不使用
就是单次只能一个事务进行对表的操作

4.多版本并发控制( MVCC )
MVCC(Multi-Version Concurrency Control,多版本并发控制)是 InnoDB 引擎实现事务隔离性的核心无锁机制。它通过为每行数据维护多个历史版本,让读写操作互不阻塞
MVCC 的核心思想
为每行数据维护多个历史版本,读操作和写操作访问不同的版本:
- 写操作:修改最新版本的数据,同时保留旧版本
- 读操作:读取事务开始时刻的历史快照,不需要加锁,也不会被写操作阻塞
最终实现:读不阻塞写,写也不阻塞读,从根本上解决了读写冲突问题。
MVCC 的实现依赖三个核心组件:行隐藏字段、undo log 回滚日志、Read View 读视图
(1)3个行隐藏字段
InnoDB 中,每一行用户数据除了我们定义的字段,还会自动添加 3 个隐藏字段,是 MVCC 的基础:

补充:还有一个隐藏的删除标记位。删除数据不是直接物理删除,只是把标记位置为已删除,后续由后台线程清理。这样删除也能形成历史版本,支持回滚

如上图,创建了一个表,插入了一个数据,select 的查询结果只有两列,实际上呢:

(2)undo log日志
undo log(回滚日志)是存储历史数据的地方,MVCC 的多版本本质上就是靠 undo log 实现的

现在我们要将这条信息的名字从张三改成李四,如下图:

那么此时假如是id=10的事务执行这条SQL语句,在 undo log 中是这样的:

现在又有一个事务11,对student表中记录进行修改(update):将age(28)改成age(38)

上面的一个一个版本,我们可以称之为一个一个的快照
(3)read view :读视图
Read View 是事务执行快照读时生成的「数据可见性快照」,用来判断当前事务能看到版本链中的哪一个版本
其实,read view 就是个类对象
class ReadView {
// 省略...
private:
/** 高水位,大于等于这个ID的事务均不可见*/
trx_id_t m_low_limit_id
我们在实际读取数据版本链的时候,是能读取到每一个版本对应的事务ID的,即:当前记录的
DB_TRX_ID 。
那么,我们现在手里面有的东西就有,当前快照读的 ReadView 和 版本链中的某一个记录的
DB_TRX_ID 。
所以现在的问题就是,当前快照读,应不应该读到当前版本记录。一张图,解决所有问题!
/** 低水位:小于这个ID的事务均可见 */
trx_id_t m_up_limit_id;
/** 创建该 Read View 的事务ID*/
trx_id_t m_creator_trx_id;
/** 创建视图时的活跃事务id列表*/
ids_t m_ids;
/** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,
* 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/
trx_id_t m_low_limit_no;
/** 标记视图是否被关闭*/
bool m_closed;
// 省略...
};
Read View 的四个核心参数

a.核心规则:数据可见性判断
当事务执行快照读时,会从版本链的最新版本 开始,依次拿每条记录的 DB_TRX_ID 和 Read View 对比,判断该版本对当前事务是否可见
判断步骤(按优先级)
- 如果 DB_TRX_ID < up_limit_id
该事务在快照生成之前就已经提交了 → 可见 - 如果 DB_TRX_ID >= low_limit_id
该事务是在快照生成之后才开启的 → 不可见 - 中间范围(up_limit_id ≤ DB_TRX_ID < low_limit_id)
判断DB_TRX_ID是否在m_ids列表中:- 在列表中 :该事务在生成快照时处于活跃未提交状态 → 不可见
- 不在列表中 :该事务在生成快照前已经提交 → 可见

b.两种读取模式
MySQL 的数据读取分为两类,MVCC 只作用于快照读
- 快照读
读取数据的历史版本,不加锁,靠 MVCC 实现。
- 对应操作:普通的
select语句 - 特点:不会阻塞写操作,性能极高
- 适用场景:绝大多数普通查询场景
- 当前读
读取数据的最新版本,需要加锁。
- 对应操作:
insert、update、delete写操作select ... for update(排他锁)select ... lock in share mode(共享锁)
- 特点:会阻塞其他写操作,保证读取到的是最新数据
- 适用场景:需要严格实时性的写前校验场景
c.RC 与 RR 隔离级别的本质区别
读提交(RC)和可重复读(RR)两个隔离级别,核心差异就在于 Read View 的生成时机不同
- 读提交(Read Committed,RC)
规则:每次执行快照读,都会生成一个全新的 Read View
每次查询都会获取当前最新的快照,因此能看到其他事务刚刚提交的修改。
- 优点:数据实时性高
- 缺点:同一个事务内,两次相同查询可能得到不同结果 → 产生不可重复读问题
- 可重复读(Repeatable Read,RR)
规则:事务中第一次执行快照读时生成 Read View,之后所有快照读都复用这同一个视图
整个事务看到的都是同一个时间点的数据快照,无论其他事务是否提交修改,当前事务的查询结果始终一致。
- 优点:同一个事务内读取结果始终一致 → 实现了可重复读
- 配合 Next-Key 锁(间隙锁 + 行锁),进一步解决了幻读问题
- 这是 MySQL 的默认隔离级别

三.视图
视图是一种虚拟表,本身不实际存储数据,其内容完全由一条 SELECT 查询语句定义
视图与底层基表数据实时联动:
- 基表数据发生变化,视图的查询结果会同步更新
- 对简单视图执行增删改操作,会直接作用到底层基表
创建视图
语法:
CREATE VIEW 视图名 AS SELECT查询语句;
视图的 DML 操作(修改基表)
对于单表、无聚合、无分组的简单视图,支持执行 INSERT、UPDATE、DELETE 操作,修改会直接作用到底层基表。
删除视图
DROP VIEW 视图名;
结尾
这一期对于MySQL的学习就到这里了,希望对你的学习有所帮助。感谢你的点赞、关注。我主页里有更好康的呦!