深入 MySQL 内核:从 B+ 树索引到 InnoDB MVCC 并发控制机制解析

在构建高并发的 C++ 后端微服务时,数据库往往是性能的最终瓶颈。深入理解 InnoDB 的索引结构与并发控制机制,是编写高性能、数据一致性代码的必修课。本文将从底层 Page 结构出发,深度剖析 MVCC 的实现原理。

索引

1.索引的价值在于提高数据在海量数据中的检索速度。

2.MySQL 中的数据文件,是以 Page(页, 16KB) 为基本单位保存在磁盘中的;同时,IO(磁盘与内存的数据交互)的基本单位也是 Page。(为什么是Page:根据局部性原理可以得知用户下一次访问的数据很可能就在本次访问数据的周围,如果只是一个扇区一个扇区(512字节)地访问效率太低)

3.任何 CRUD 操作都需要先通过 CPU 计算,确定数据的插入 / 修改 / 查询位置;

CPU 只能处理内存中的数据,因此必须先将磁盘中的 Page 数据加载到内存中;

操作完成后,内存中的数据会按照特定的刷新策略(如 checkpoint)同步回磁盘;

特定时间内,数据会同时存在于磁盘(持久化) 和内存(临时操作) 中,二者的交互就是 IO 操作。

4.MySQL 服务器在内存中申请的一块超大内存空间,专门用于缓存磁盘中的 Page 数据;

这块缓冲区的作用:作为磁盘和 CPU 之间的 "中间层",承接所有数据的 IO 交互;

核心目标:通过缓存减少磁盘 IO 次数,提升 MySQL 整体执行效率(磁盘 IO 是数据库性能瓶颈,内存操作远快于磁盘)。

即:通过尽可能减少系统与磁盘的 IO 次数来提高数据库的操作效率。

5.如果一张表中有多个索引,此时就会有多张page构成的目录树结构(根据不同的存储引擎的叶子节点存储的东西不一样)。

多页目录

(1).page会被MySQL描述管理起来,里边存储好按照主键进行排序后的数据。

(2).正常插入就是++前/后++正常插入即可,如果要插入的位置是中间的page且该page装不下,则此时会发生页分裂,并同步结构到上层的页目录中。(删除中间的page中的大量数据可能会导致页合并)

此时就可以通过索引进行快速定位在O(log n)时间内获取查询结果,同时还能够一次性将一整个page的数据拷贝到内存中从而减少和磁盘的IO次数提高性能。

单页目录

6.MySQL InnoDB 所用B+树特点:

叶子节点:唯一用于保存实际数据(INNODB存储数据页,这种存放实际数据的是聚簇索引)(MyISAM存储数据页的地址,这种存放非实际数据而只是实际数据地址的是非聚簇索引);

非叶子节点(根节点 / 中间节点):仅保存下层节点的起始索引值 + 子节点 Page 指针,不存储实际数据(即目录页);

中间节点的索引值由上层节点汇总保存,上层节点再由更上层节点保存。

7.为什么选择B+树不选择B树:

B+树叶子节点相连便于范围查找;

B+树非叶子结点不存储数据,使得高度更低矮从而使得IO操作次数更少。

8.对于聚簇索引,因辅助索引(就是除主键外的索引,MySQL在表中无用户指定主键时会生成默认主键)生成的表的叶子结点中不存放实际的数据页,而是存放的是该数据在主键中的key值,而后通过key值对因主键形成的树进行二次检索(这个过程叫回表查询)。

对于非聚簇索引,因辅助索引生成的表的叶子结点中存放的还是数据页的地址。

9.最左匹配原则和复合索引

创建复合索引:idx_abc (a, b, c)。

复合索引的结构就像 "多层字典排序":比如创建复合索引 idx_abc (a, b, c),MySQL 会先按 a 排序,a 相同的行再按 b 排序,b 相同的行再按 c 排序。

注:查询时索引会从最左侧的 a 开始匹配,只有 "从左到右连续的前缀" 能用到索引;跳过中间字段会导致后续字段无法使用索引(仅能用到跳过字段前的索引部分)。

事务

为了防止各种各样的潜在错误和并发问题的出现,数据库层面设计了事务机制。

完整事务满足四个属性(ACID):

(1).原子性:一个事务的全部操作,要么全部完成要么就是一点都没完成,如果事务执行过程中发生错误,会被回滚到事务开始前的状态。

(2).一致性:事务执行的操作必须完全符合预设规则。

(3).隔离性:数据库允许多个并发事务同时对其数据进行读写和修改。隔离性可以防止多个事务并发执行时由于交叉执行而导致的数据不一致问题。

事务隔离分为四个类别:读未提交、读提交、可重复读、串行化。

(4).持久性:事务处理结束后,对数据的修改就是永久的,即使系统此时发生了故障,数据也不会改变。

事务的提交方式

手动提交、自动提交。

自动提交情况下,单条 SQL 语句会被 MySQL 自动封装成一个独立的事务。

手动提交情况下则不同。(输入begin开始,输入commit提交,输入rollback [ ]回滚 [指定保存点])

四种隔离方式的特点

注:需要开启手动提交事务。

读未提交

多个MySQL客户端同时开启事务,当其中一个客户端对数据库内容进行修改在未提交的情况下,其它客户端也能看到。但会有脏读、不可重复读、幻读问题。

脏读问题

一个事务执行过程中,读到另一个事务begin但未commit执行的操作。

读提交

多个MySQL客户端同时开启事务,当其中一个客户端对数据库内容进行修改在已提交的情况下,其它客户端在不退出当前事务的情况下能看到。但会有不可重复读、幻读问题。

不可重复读问题

在本事务(A事务)执行过程中,不同时间段的读取会得到不同的结果。

可重复读

多个MySQL客户端同时开启事务,当其中A客户端对数据库内容进行修改在已提交的情况下,其它客户端在不退出当前事务的情况下不能看到,只有当其将事务提交后才能看到A事务操作后的结果。但会有幻读问题。

幻读问题

(MySQL通过 MVCC(解决 读-写 )+锁(解决 写-写 ) 解决了这个问题)一般数据库在多个事务并发时,在不同时间段的读取仍然会读到 因其它事务insert提交后改动 的数据(虽然本事务并没有提交),如同产生了幻觉。

串行化

隔离的最高级别,强制将事务进行排序,使之不可能冲突(加共享锁)。但效率太低,基本不用。

MVCC(多版本并发控制)

1.MVCC用来解决读-写场景下的并发性问题。

2.MVCC是行级的,其生成的快照是行级的。

3.创建表的时候,MySQL会为其额外创建三个隐藏列:

DB_TRX_ID:记录最近修改本 行记录 的事务ID。

DB_ROLL_PTR:回滚指针,记录该 行记录 的上一个 行记录 版本。

DB_ROW_ID:MySQL创建的隐藏主键。

4.对于写-写操作来说,是通过锁来实现一致性;

而对于读-读操作不需要额外操作;

而对于读-写操作,则是通过undo log+快照+read view理论完成。

对于写操作,其使用的是当前读(当前读会无视 MVCC 的快照,强制读取该记录的最新已提交版本,并对其加锁,然后再进行修改)(修改行 的DB_ROLL_PTR会指向 该行上一个版本的记录 ,在事件异常终止时该行会通过历史版本链执行反向操作回滚到初始版本);

而读操作如果执行的是当前读,那么也要加锁;如果执行的是快照读,会生成一个逻辑快照(Read View),并在同一条版本链中顺藤摸瓜,找到对自己可见的那个历史版本。

读操作知晓自己能读取到的所有版本后,获得数据的过程 就是先获取最新版本的表中的 行记录 ,然后如果其中的子表数据中的隐藏三列中的DB_TRX_ID不符合read view的要求,此时就会通过DB_ROLL_PTR进行历史的遍历知道获得满足read view要求的快照版本行。(且这个版本必须对该读操作所属的事务而言是可见的)(当事务commit后,其undo log会延迟清理)

read view是快照读操作的前一刻,系统为当前事务分配的逻辑视图(其内部明确了本事务的读操作能看到什么快照版本的数据),其内部有四个变量

快照版本中事务ID小于min_trx_id的都能看,大于等于max_trx_id的都不能看,在m_ids中的不能看,最终能看到的版本是max_trx_id往下的最大值&&该最大值需不在m_ids中。

5.对于RR(可重复读)而言,同一事务在第一次select后,其的read view就已经确定了;

而对于RC(读已提交)而言,同一事务在第一次select后,其的read view会不断更新。

6.undo log里边保存了因历史上各个事务修改而留下的 旧的行版本数据,而read view保存的是快照读操作能够读取到的最新数据应该满足的条件。快照读是在整个undo log中顺藤摸瓜,不是完全遍历,写操作是仅针对本事务的undo log进行操作。

相关推荐
Crazy________2 小时前
力扣113个mysql简单题解析(包含plus题目)
mysql·算法·leetcode·职场和发展
jason_renyu3 小时前
数据库关联查询(JOIN)完全指南
数据库·数据库关联查询·关联查询指南·数据库关联查询学习
是码龙不是码农3 小时前
MySQL 锁的完整分类与详解
数据库·mysql·
..过云雨3 小时前
【MySQL】3. MySQL库的操作
数据库·mysql
wregjru3 小时前
【操作系统】12.Linux 多线程同步与互斥详解
数据库·mysql
小李独爱秋3 小时前
模拟面试:简述一下MySQL数据库的备份方式。
数据库·mysql·面试·职场和发展·数据备份
難釋懷4 小时前
Redis消息队列-基于Stream的消息队列-消费者组
数据库·redis·缓存
四七伵4 小时前
数据库必修课:MySQL金额字段用decimal还是bigint?
数据库·后端
diaya5 小时前
麒麟V10 x86系统安装mysql
数据库·mysql