目录
为什么InnoDB使用B+树作为底层
B+树的叶子节点是单向链表还是双向链表?如果从大值向小值检索,如何操作?
一个B+树可以存储多少数据呢?
索引为什么用B+树不用普通二叉树呢?
为什么索引不用B树用B+树
为什么用B+树 不用跳表呢
B+树的范围查找是怎么做的
B+树索引和hash索引的的区别
聚簇索引和非聚簇索引的区别
什么是回表
MRR
联合索引
覆盖索引
什么是最左前缀原则
MySQL中有哪几种锁
说说行锁
加select for update 排他锁 注意什么
执行什么命令会加间隙锁
临键锁\
乐观锁悲观锁
B+树的叶子节点是通过双向链表连接的,这样可以方便范围查询和反向遍历。
如果需要在 B+树中从大值向小值进行检索,可以先定位到最右侧节点,找到包含最大值的叶子节点。从根节点开始向右遍历树的方式实现
一棵 B+ 树能存多少数据,取决于它的分支因子和高度。在 InnoDB 中,页的默认大小为 16KB,当主键为 bigint 时,3 层 B+ 树通常可以存储约 2000 万条数据。
普通二叉树的每个节点最多有两个子节点。当数据按顺序递增插入时,二叉树会退化成链表,导致树的高度等于数据量。此时查找 id=7 就需要 7 次 I/O 操作,相当于全表扫描。而 B+ 树作为多叉平衡树,能将数亿级的数据量控制在 3-4 层的树高,能极大减少磁盘的 I/O 次数。
B+树相较于B树有三个显著优势
第一,B 树的每个节点既存储键值,又存储数据和指针,导致单节点存储的键值数量较少。一个 16KB 的 InnoDB 页,如果数据较大,B 树的非叶子节点只能容纳几十个键值,而 B+ 树的非叶子节点可以容纳上千个键值。
第二,B 树的范围查询需要通过中序遍历逐层回溯;而 B+ 树的叶子节点通过双向链表顺序连接,范围查询只需定位起始点后顺序遍历链表即可,没有回溯开销。
第三,B 树的数据可能存储在任意节点,假如目标数据恰好位于根节点或上层节点,查询仅需 1-2 次 I/O;但如果数据位于底层节点,则需多次 I/O,导致查询时间波动较大。
而 B+ 树的所有数据都存储在叶子节点,查询路径的长度是固定的,时间稳定为 O(logN),对 MySQL 在高并发场景下的稳定性至关重要。
跳表本质上还是链表结构,只不过把某些节点抽到上层做了索引。
一条数据一个节点,如果需要存放 2000 万条数据,且每次查询都要能达到二分查找的效果,那么跳表的高度大约为 24 层(2 的 24 次方)。
在最坏的情况下,这 24 层数据分散在不同的数据页,查找一次数据就需要 24 次磁盘 I/O。
而 2000 万条数据在 B+树中只需要 3 层就可以了。
先通过索引路径定位到第一个满足条件的叶子节点,然后顺着叶子节点之间的链表向右/向左扫描,直到超过范围。
B+ 树索引支持范围查询、有序扫描,是 InnoDB 的默认索引结构。
Hash 索引只支持等值查找,速度快但功能弱,常见于 Memory 引擎。
聚簇索引的叶子节点存储了完整的数据行,数据和索引是在一起的。InnoDB 的主键索引就是聚簇索引,叶子节点不仅存储了主键值,还存储了其他列的值,因此按照主键进行查询的速度会非常快。每个表只能有一个聚簇索引,通常由主键定义。
非聚簇索引的叶子节点只包含了主键值,需要通过回表按照主键去聚簇索引查找其他列的值,唯一索引、普通索引等非主键索引都是非聚簇索引。
当使用非聚簇索引进行查询时,MySQL 需要先通过非聚簇索引找到主键值,然后再根据主键值回到聚簇索引中查找完整数据行,这个过程称为回表。
回表通常需要访问额外的数据页,如果数据不在内存中,还需要从磁盘读取,增加 I/O 开销。
什么情况下必然会触发回表?
第一,当查询字段不在非聚簇索引中时,必须回表到主键索引获取数据。
第二,查询字段包含非索引列(如 SELECT *),必然触发回表。
MRR 是 InnoDB 为了解决回表带来的大量随机 IO 问题而引入的一种优化策略。它会先把非聚簇索引查到的主键值列表进行排序,再按顺序去主键索引中批量回表,将随机 I/O 转换为顺序 I/O,以减少磁盘寻道时间。
联合索引就是把多个字段放在一个索引里,但必须遵守“最左前缀”原则,只有从第一个字段开始连续使用,索引才会生效。
联合索引会按字段顺序构建B+树。例如(age, name)
索引会先按照 age 排序,age 相同则按照 name 排序,若两者都相同则按主键排序,确保叶子节点无重复索引项。
联合索引属于非聚簇索引,叶子节点存储的是联合索引各列的值和对应行的主键值,而不是完整的数据行。查询非索引字段时,需要通过主键值回表到聚簇索引获取完整数据。
覆盖索引指的是:查询所需的字段全部都在索引中,不需要回表,从索引页就能直接返回结果。
最左前缀原则指的是:MySQL 使用联合索引时,必须从最左边的字段开始匹配,才能命中索引。
查询条件 | 是否触发索引? | 说明 |
---|---|---|
WHERE A = 1 | ✅ 是 | 使用索引的第一列 |
WHERE A = 1 AND B = 2 | ✅ 是 | 使用索引的前两列 |
WHERE A = 1 AND B = 2 AND C = 3 | ✅ 是 | 使用索引的全部列 |
WHERE B = 2 | ❌ 否 | 跳过左侧列 A,索引失效 |
WHERE B = 2 AND C = 3 | ❌ 否 | 无左侧列,索引失效 |
WHERE A = 1 AND C = 3 | ⚠️ 部分生效 | 仅用 A 列,C 列无法利用索引优化 |
范围查询后的列不能用索引了
MySQL 中有多种类型的锁,可以从不同维度来分类,按锁粒度划分的话,有表锁、行锁,页锁。
按照加锁机制划分的话,有乐观锁和悲观锁。按照兼容性划分的话,有共享锁和排他锁
表锁:锁定整个表,资源开销小,加锁快,但并发度低,不会出现死锁;适合查询为主、少量更新的场景(如 MyISAM 引擎)。
行锁:锁定单行或多行,开销大、加锁慢,可能出现死锁,但并发度高(InnoDB 默认支持)。
有记录锁(Record Lock):锁定索引中的具体记录;间隙锁(Gap Lock):锁定索引记录之间的间隙,防止幻读;临键锁(Next-Key Lock):结合记录锁和间隙锁,锁定一个左开右闭的区间(如 (5, 10]
)。
共享锁(S锁/读锁),允许多个事务同时读取数据,但阻塞写操作。语法:SELECT ... LOCK IN SHARE MODE
排他锁(X锁/写锁),独占数据,阻塞其他事务的读写
乐观锁假设冲突少,通过版本号或 CAS 机制检测冲突
悲观锁假设并发冲突频繁,先加锁再操作SELECT FOR UPDATE
。
行锁是 InnoDB 存储引擎中最细粒度的锁,它锁定表中的一行记录,允许其他事务访问表中的其他行。
底层是通过给索引加锁实现的,这就意味着只有通过索引条件检索数据时,InnoDB 才能使用行级锁,否则会退化为表锁。
行锁又可以细分为记录锁、间隙锁和临键锁三种形式。
第一,必须在事务中使用,否则锁会立即释放。
第二,使用时必须注意是否命中索引,否则可能锁全表。
在可重复读隔离级别下,执行 FOR UPDATE / LOCK IN SHARE MODE 等加锁语句,且查询条件是范围查询时,就会自动加上间隙锁。
临键锁是记录锁和间隙锁的结合体,锁住的是索引记录和索引记录之间的间隙。
MySQL 默认的行锁类型就是临键锁。当使用唯一索引的等值查询匹配到一条记录时,临键锁会退化成记录锁;如果没有匹配到任何记录,会退化成间隙锁。
悲观锁是一种"先上锁再操作"的保守策略,它假设数据被外界访问时必然会产生冲突,因此在数据处理过程中全程加锁,保证同一时间只有一个线程可以访问数据。
乐观锁会假设并发操作不会总发生冲突,属于小概率事件,因此不会在读取数据时加锁,而是在提交更新时才检查数据是否被其他事务修改过。
悲观锁的关键点:
SELECT ... FOR UPDATE
锁定行,确保其他事务必须等待当前事务完成才能操作该行数据;乐观锁的关键点: