Skip to content

GPT学习

INFO

这里是gpt给我的学习计划。

第一周

MySQL 为什么要用索引?B+ 树索引为什么比红黑树、哈希更适合数据库?

你可以把一张数据库表想成一本很厚的书。

没有索引:你查一个人,就得从第一页翻到最后一页,叫全表扫描。 有索引:相当于先看目录,再直接跳到那一页,速度会快很多。

那为什么 MySQL 常用 B+ 树 呢?

因为数据库的数据很多,放不进内存,经常要从磁盘读。 而磁盘读取最怕的不是“算得慢”,而是“读的次数太多”。

B+ 树的特点是:

  • 一层能存很多“路标”,树会很矮
  • 树越矮,查一次数据要访问的磁盘页越少
  • 叶子节点天然有序,特别适合 范围查询、排序、分页

所以数据库喜欢 B+ 树,本质上是:尽量减少磁盘 IO,同时兼顾范围查询。

补充

InnoDB 的主键索引是聚簇索引,数据本身就存放在主键索引的叶子节点;普通二级索引叶子节点存的是主键值,所以通过二级索引查数据时,往往还要再回主键索引查一次,这个过程叫回表。 而 B+ 树的非叶子节点只存索引键和指向子节点的指针,不存数据,这样可以在内存中存更多的索引键,减少树的高度,从而减少磁盘访问次数,提高查询效率。

追问 1:为什么不用红黑树?

因为红黑树是二叉树,每个节点只能分两个方向,树会很高。 数据库数据量大时,树高一上来,就意味着查一次可能要做很多次磁盘 IO,不适合数据库这种磁盘场景。 而 B+ 树是多叉树,树更矮,IO 次数更少。

追问 2:为什么不用哈希?

哈希适合 等值查询,比如 where id = 10。 但它有几个问题:

  • 不支持范围查询,比如 id > 10
  • 不支持排序
  • 不能很好利用最左前缀这类特性
  • 哈希冲突还会影响性能

所以数据库通用索引更偏向 B+ 树。

追问 3:什么是聚簇索引,什么是非聚簇索引?

在 InnoDB 里:

  • 聚簇索引(主键索引):叶子节点直接存整行数据
  • 二级索引(普通索引):叶子节点存的是主键值,不是整行数据

所以:

  • 查主键,通常直接就拿到数据
  • 查普通索引,可能先找到主键,再去主键索引拿整行,这叫回表

追问 4:什么情况下索引会失效?

高频答案先记这几个:

  • 对索引列做函数、计算、类型转换
  • 没有满足最左前缀原则
  • like 以 % 开头
  • or 使用不当:一边有索引,一边没有索引,可能导致全表扫描;两边是俩索引,可能只能用一个索引,另一个索引失效;两边都没有索引,肯定全表扫描了。
  • 数据区分度太低,优化器觉得全表扫更划算
  • 隐式类型转换,比如字符串列拿数字去查

追问 5:什么是最左前缀匹配?

联合索引比如 (a, b, c),能用上的前提通常是从最左边开始连续匹配。 也就是:

  • a ✅
  • a, b ✅
  • a, b, c ✅
  • b, c ❌
  • a, c 一般只能部分利用

因为联合索引在底层是按 (a,b,c) 的顺序排好序的。

MySQL 事务是什么?ACID 是什么?四种隔离级别怎么理解?

事务是一组逻辑操作单元,要么全部成功提交,要么全部失败回滚。InnoDB 通过 ACID 保证事务特性:原子性保证要么全做要么全撤销,一致性保证事务前后数据处于合法状态,隔离性解决并发事务互相干扰的问题,持久性保证提交后的结果尽量可靠保存。InnoDB 支持四种隔离级别,默认是 REPEATABLE READ。READ UNCOMMITTED 可能出现脏读;READ COMMITTED 解决脏读,但因为每次读都是新快照,所以还可能有不可重复读和幻读;REPEATABLE READ 下同一事务的普通 SELECT 读取第一次读建立的快照,能避免不可重复读,InnoDB 还会结合 next-key lock 防止幻读;SERIALIZABLE 隔离性最强,但并发性能最差。

追问 1:MySQL 默认隔离级别是什么?

InnoDB 默认是 REPEATABLE READ。

追问 2:READ COMMITTED 和 REPEATABLE READ 最大区别是什么?

核心区别就是:

  • READ COMMITTED:每次一致性读都是新快照
  • REPEATABLE READ:同一事务里一致性读复用第一次读建立的快照

面试一句话:RC 是语句级快照,RR 更接近事务级快照。

追问 3:什么叫“快照读”?

普通 SELECT 在 READ COMMITTED 和 REPEATABLE READ 下默认是 consistent nonlocking read,也就是一致性非加锁读。

它不会给读到的数据加锁,而是通过多版本机制去看某个时间点的快照。MySQL 文档对 consistent read 的定义就是:用 multi-versioning 给查询呈现某个时间点的数据库快照。

追问 4:什么叫“当前读”?

这个你先有印象就行:

  • 快照读:普通 SELECT
  • 当前读:读最新版本并且可能加锁,比如: SELECT ... FOR UPDATE、SELECT ... FOR SHARE、UPDATE、DELETE

这些加锁读在事务提交或回滚时释放锁,而且需要关闭自动提交或显式开启事务。

Select ... For Update的用途

  1. 实现分布式锁:在事务里 SELECT ... FOR UPDATE 某行,如果能成功加锁,就说明抢到锁了;如果被别人锁住了,就会等锁释放或者超时。
  2. 乐观锁的补充:先用普通 SELECT 读数据,做一些业务逻辑判断后,再用 SELECT ... FOR UPDATE 验证数据没有被别人改过,最后再 UPDATE。
  3. 处理库存等并发修改的场景:先 SELECT ... FOR UPDATE 锁住相关记录,确保在修改库存时不会有其他事务同时修改,避免超卖。

追问 5:什么是 gap lock?什么是 next-key lock?

这是你后面学“锁”和“MVCC”一定会遇到的。

  • gap lock:锁的是“索引记录之间的空隙”,主要作用是防止别人往这个区间插入新记录。MySQL 文档明确说 gap lock 的目的就是阻止插入。
  • next-key lock:记录锁 + 前面的 gap lock,也就是“锁住这条记录,同时锁住它前面的间隙”。InnoDB 默认在 REPEATABLE READ 下对搜索和索引扫描使用 next-key locks,用来防止 phantom rows。

乐观锁和悲观锁分别是什么?version、stock > 0、for update 各算哪一种?

乐观锁和悲观锁的核心区别在于是否先加锁。悲观锁假设冲突很多,所以会先锁住数据再处理,MySQL 里典型做法是 select ... for update,它属于 locking read,会读取最新值并加锁。乐观锁假设冲突不多,所以读的时候不加锁,而是在更新时通过条件校验来判断数据有没有被别人改过,常见实现是版本号 version 或条件更新,比如 update ... where stock > 0 或 where version = ?。悲观锁更适合竞争激烈、必须保证中间过程不被打断的场景;乐观锁更适合读多写少、冲突不高、追求吞吐的场景。

MVCC 到底是什么?为什么普通 SELECT 不加锁也能读?

面试背这个: MVCC 是多版本并发控制,主要解决普通 SELECT 的一致性读问题;而 INSERT / UPDATE / DELETE 负责生产或推进这些版本。InnoDB 会为被修改的行保留旧版本信息,旧版本主要通过 undo log 保存;每行还会维护像 DB_TRX_ID、DB_ROLL_PTR 这样的隐藏字段。普通 SELECT 在 READ COMMITTED 和 REPEATABLE READ 下默认属于一致性非加锁读,数据库会根据事务的快照规则判断当前行的哪个版本对它可见,因此普通查询通常不需要加锁也能读到一致结果。READ COMMITTED 下每次一致性读都会读取新的快照,而 REPEATABLE READ 下同一事务内的普通查询会复用第一次读建立的快照。MVCC 主要服务于快照读,提高读写并发;对于 SELECT ... FOR UPDATE、UPDATE、DELETE 这类当前读或加锁语句,则还要结合行锁、gap lock、next-key lock 来保证并发正确性。

MVCC 让快照读成为可能;至于快照是“每次新建”还是“复用第一次的”,取决于隔离级别。

  • RC(READ COMMITTED):每次 consistent read 都会读取一个新的快照
  • RR(REPEATABLE READ):同一事务里的所有 consistent read 都复用第一次读建立的快照

MVCC = 多版本并发控制。 你可以把它理解成:数据库不会只保存“当前这一版数据”,还会保留旧版本的信息,这样不同事务就能在并发时各自看到自己该看到的版本。 InnoDB 官方就直接说它是一个 multi-version storage engine,会保存被修改行的旧版本信息来支持并发和回滚;这些旧版本信息保存在 undo tablespaces 的 rollback segment 里。

  1. 隐藏字段

InnoDB 在每行里会额外维护隐藏信息。官方文档列出了至少这两个关键字段:

DB_TRX_ID:最后一次修改这行的事务 ID DB_ROLL_PTR:指向 undo log 的指针

  1. undo log

undo log 里保存旧版本所需的信息。 如果当前版本不该给你看,数据库就可以沿着 undo 去还原更早的版本。官方明确说,InnoDB 使用 rollback segment 里的信息去构建 earlier versions of a row for a consistent read。

  1. Read View

你可以把它理解成:“我这个事务此刻观察世界的时间点规则”。 官方文档虽然在这几页里没有把 Read View 细节全展开,但明确说了:在 REPEATABLE READ 下,同一事务内的 consistent reads 读取的是第一次读建立的 snapshot;在 READ COMMITTED 下,每次 consistent read 都会读取自己的 fresh snapshot。

MVCC 是否能解决幻读?

MVCC 主要解决的是普通查询的一致性读问题。 在 InnoDB 默认的 REPEATABLE READ 下,普通 SELECT 依靠 snapshot 做到可重复读;而对于 locking reads、UPDATE、DELETE 这类会扫描索引范围的语句,InnoDB 会用 gap lock / next-key lock 来防止范围内出现新的插入,从而防止 phantom rows。官方文档说明了两件事:一是 REPEATABLE READ 下普通查询读第一次快照;二是默认会对搜索和索引扫描使用 next-key locks 来防止 phantom rows。

所以面试里更稳的说法是:普通查询靠 MVCC;涉及当前读和范围修改时,再结合 next-key lock 处理幻读。

追问 1:为什么 MVCC 能提高并发?

因为如果没有 MVCC, 普通读为了保证一致性,很多时候就得加锁;一旦读也大量加锁,读写就会互相卡住。

而 InnoDB 的 consistent read 默认不加锁,所以:

  • 普通读不容易阻塞写
  • 写也不一定非得把普通读全堵住
  • 在很多读多写少的业务里,并发会好很多

这就是为什么大家总说:

MVCC 用空间换时间,用多版本换并发。这句话是工程总结,不是官方原句,但和官方机制描述是一致的:为了支持 consistent read,InnoDB 保留旧版本信息,并可通过 undo 构建早期版本。

对于RR

  • 如果流程是 SELECT → 你自己 UPDATE 同一行 → SELECT,第二次 SELECT 会看到你自己改后的值。
  • 如果中间是别人改了并提交,而你自己没改这行,那么你后面的普通 SELECT 仍然通常看到第一次快照里的旧值。
  • 如果中间别人改了,而你后面的 UPDATE 又去操作了那行,那么这条 UPDATE 走的是更“当前”的加锁/修改语义,不完全受第一次快照约束;一旦它被你更新成功,后面的普通 SELECT 就能看到你刚更新后的结果。 官方把这种现象明确列为 RR 下的一种 anomaly。

MySQL 里的行锁、间隙锁、next-key lock,到底分别锁什么?

InnoDB 的行锁本质上是索引记录锁。记录锁锁的是某个索引记录;间隙锁锁的是索引记录之间的空隙,只用来阻止插入;next-key lock是记录锁加前面的间隙锁。RR 下 InnoDB 默认对搜索和索引扫描使用 next-key lock 来防止幻读;而 RC 下通常关闭 gap lock,所以更多是只锁记录,不锁 gap。对于唯一索引上的唯一条件,通常只需要记录锁;对于范围查询或非唯一条件,通常会锁扫描到的索引范围。

追问 1:为什么叫“行锁”,但实际上是索引记录锁?

InnoDB 的行锁是基于索引实现的,所以它锁的不是物理行,而是索引记录。官方文档明确说,InnoDB 的行锁是索引记录锁,锁住的是索引记录而不是物理行。也就是说,如果你对一个表没有索引,InnoDB 就只能退化成全表锁了。

追问 2:INSERT 又是什么锁?

这个也很高频。

官方说:INSERT 对插入进去的那一行,设置的是排他的 index-record lock,这不是 next-key lock,不会锁前面的 gap。但在真正插入前,会先加一种 insert intention gap lock,表示“我打算往这个 gap 里插入”

官方还特别说明:如果两个事务都往同一个 gap 里插,但插入的位置不同,它们不一定互相阻塞。

白话理解:就是先举手说“我要往这个空隙里塞一条记录。”,如果你和别人不是要塞到完全冲突的位置,可能可以并行。

追问 3:追问 1:SELECT ... FOR UPDATE 一定只锁最终结果行吗?

不一定。

官方说,UPDATE、DELETE、locking read 一般会对扫描到的索引记录加锁,不是只看最终 WHERE 过滤后剩下的那些行;因为 InnoDB 知道的是“扫描了哪些索引范围”,而不是精确保留你写的 WHERE 条件。

追问 4:为什么范围查询更容易造成锁冲突?

因为它锁的不是一个点,而是一个区间,甚至连区间里的 gap 都可能一起锁住。

追问 5:为什么唯一索引很重要?

因为唯一索引 + 唯一条件通常可以把锁精确到一条记录,而不是一整个范围。如果没有唯一索引,很多 UPDATE、DELETE、SELECT ... FOR UPDATE 就只能锁一个范围,导致更多的锁冲突和性能问题。