Skip to content

00 · 数据库导读:数据怎么存下来还不乱 ​

数据库这块是"四个模块的交汇点":索引用到数据结构,事务用到操作系统的日志和刷盘,并发用到锁,查询用到网络往返。

一句话本质 ​

数据库要同时解决四个问题:存得下(存储)、找得快(索引)、不出错(事务)、扛得住并发(锁/MVCC)。

生活类比:图书馆 ​

数据库图书馆
表一个书架
行一本书
索引索引柜里的卡片(另存一份,按序排好)
聚簇索引书按编号顺序摆在架上(书本身就是目录)
非聚簇索引单独的卡片柜,卡片上写着"书在几排几号"
事务一次借还流程:要么全部办完,要么都不算
redo log操作日志:先记"我做了什么",万一出事能重做
undo log备份:万一要撤销,能还原旧值
锁借出登记本:我借走了别人不能拿
MVCC同一本书存了多个版本的副本,各看各的,互不干扰
隔离级别允许你看到别人"改到一半"的书吗

模块地图:5 篇怎么接 ​

+-- 01 索引:怎么找得快
|      <- 依赖:B+ 树(数据结构 03-03)
|
+-- 02 事务:怎么保证不出错
|      <- 依赖:日志、刷盘(操作系统 IO)
|      ACID 里的 C 和 I,靠 03 和 04 实现
|
+-- 03 MVCC:用"多版本"减少锁(乐观思路)
+-- 04 锁与死锁:用"锁"保证互斥(悲观思路)
|      两者是解决同一问题的两条路
|
+-- 05 SQL 执行与优化:怎么把上面都用对

关键理解:03(MVCC)和 04(锁)是解决同一问题的两条路。

  • 锁:悲观。我怕你改,先锁住,你等着。
  • MVCC:乐观。我不锁你,我给你看旧版本,你随便读。

MySQL 的 InnoDB 是两者混用:读用 MVCC(不加锁),写用锁。这就是为什么"读不阻塞写,写不阻塞读"。

一条主线:一条 SQL 是怎么执行的 ​

① 客户端发 SQL > 网络传到服务端(网络篇)
   v
② 连接器:验证账号密码、权限
   v
③ 查询缓存(MySQL 8.0 已移除,因为命中率太低)
   v
④ 解析器:词法分析、语法分析,生成语法树
   v
⑤ 优化器:选索引、决定 join 顺序,生成执行计划
   v  < 这一步决定了 SQL 快不快,EXPLAIN 看的就是它
⑥ 执行器:按执行计划调用存储引擎接口
   v
⑦ 存储引擎(InnoDB):
   a. 从 Buffer Pool 找页(内存里的缓存)
   b. 找不到 > 从磁盘读一页(16KB)到 Buffer Pool(一次 IO)
   c. 在页里用 B+ 树定位到行
   d. 读操作:用 MVCC 判断这个版本你可见吗
   e. 写操作:加锁 > 改 Buffer Pool > 写 undo log > 写 redo log(prepare)> 写 binlog > redo log(commit)
   v
⑧ 返回结果

这条线把四个模块全串起来了:

  • ① 是网络
  • ⑦a/b 是操作系统(虚拟内存、页缓存、IO 模型)
  • ⑦c 是数据结构(B+ 树)
  • ⑦d/e 是事务、MVCC、锁

学这个模块的方法 ​

  1. 所有"为什么"都要落到"磁盘很慢"这个前提上。数据库的绝大多数设计(B+ 树、Buffer Pool、日志、批量刷盘)都是在对抗磁盘的慢。
  2. 一定要跑 EXPLAIN。看不懂执行计划,索引那篇等于没学。
  3. 画版本链。MVCC 是最抽象的一篇,必须画出"一行数据的多个版本 + ReadView 怎么判断可见性"。
  4. 别背隔离级别的定义,背"每个级别解决了哪个异常"。

和其他模块的连接 ​

自测 3 问 ​

Q1:数据库和文件系统到底差在哪?

点开看答案

文件系统只负责"存",数据库在"存"之上加了四层能力:

  1. 结构化查询:文件系统只能按路径找;数据库能"找出所有 age > 20 且 city = 北京 的记录",靠索引加速。
  2. 并发控制:多个进程同时写同一个文件会互相覆盖;数据库用锁和 MVCC 保证正确。
  3. 事务:文件写到一半断电就坏了;数据库能保证"要么全成,要么全不成"。
  4. 约束与一致性:主键唯一、外键、非空——文件系统不管这些。

一句话:文件系统是"字节的容器",数据库是"有语义、可并发、可回滚的字节容器"。

Q2:为什么数据库要自己管理缓存(Buffer Pool),不用操作系统的页缓存?

点开看答案

三个原因:

  1. 可控的淘汰策略。OS 的页缓存用 LRU,但它不知道"哪些页是热数据"。数据库知道自己的访问模式,可以用改良 LRU(比如 InnoDB 的"分区 LRU",防止全表扫描把热数据冲掉)。
  2. 需要预读和批量。数据库知道"读了索引页,大概率要读数据页",可以主动预读。
  3. 避免双重缓存。如果既走 OS 页缓存又走 Buffer Pool,同一份数据在内存里存两份,浪费。所以 InnoDB 通常用 O_DIRECT 绕过 OS 缓存。

代价:数据库要自己实现缓存淘汰、刷脏页、崩溃恢复——复杂度大幅上升。但对性能敏感的场景,这个代价是值得的。

Q3:为什么 MySQL 8.0 移除了查询缓存?

点开看答案

因为命中率太低,维护成本太高。

查询缓存要求"SQL 文本完全一致"才命中——多一个空格、大小写不同、参数不同,都不命中。

更致命的是:任何对表的写操作,都会让这张表的所有查询缓存失效。在写多的场景下,缓存刚建好就被清空,几乎全是无效功。

而且每次检查缓存都要加全局锁,反而成为性能瓶颈。

替代方案:应用层缓存(Redis)+ InnoDB 的 Buffer Pool(缓存数据页而不是查询结果)。后者粒度更细,写操作只需要让相关的页失效。

类比失效处 / 易错点 ​

  • "图书馆"类比会让人以为索引和数据是分开的两份。实际上聚簇索引的叶子节点就是数据本身,没有"卡片柜"。
  • "事务"不是"锁"。事务是"一组操作的原子性保证",锁只是实现隔离的一种手段(MVCC 就不用锁)。
  • 数据库的"行"不是"一整块连续存储"。行存在页里,页之间可以不连续,靠 B+ 树组织。
  • 不要以为"加了索引就一定快"。索引有维护成本(每次写要更新索引),且区分度低时(比如性别字段)几乎没用。
  • "ACID"里的 C(一致性)不是数据库实现的,它是应用和数据库共同保证的目标。A、I、D 是手段,C 是结果。这个区分很多人答错。

✅ 验收清单 ​

  • 能说出数据库要解决的四个核心问题(存、找、不错、并发)
  • 能画出 5 篇的依赖图,并说出"MVCC 和锁是同一问题的两条路"
  • 能完整讲出"一条 SQL 的执行流程"(至少 6 个步骤)
  • 能说出 Buffer Pool 存在的理由,以及它为什么不用 OS 页缓存
  • 能解释"数据库和文件系统的本质区别"(要说出 4 点)
  • 能说清"ACID 里 C 不是数据库实现的"这句话的含义
  • 合上书,用自己的话讲 3 分钟,中途不卡壳