深色模式
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、锁
学这个模块的方法
- 所有"为什么"都要落到"磁盘很慢"这个前提上。数据库的绝大多数设计(B+ 树、Buffer Pool、日志、批量刷盘)都是在对抗磁盘的慢。
- 一定要跑 EXPLAIN。看不懂执行计划,索引那篇等于没学。
- 画版本链。MVCC 是最抽象的一篇,必须画出"一行数据的多个版本 + ReadView 怎么判断可见性"。
- 别背隔离级别的定义,背"每个级别解决了哪个异常"。
和其他模块的连接
- < 来自 树与堆:B+ 树是索引的基础,树高 = 磁盘 IO 次数
- < 来自 IO 模型与零拷贝:等设备的时候,CPU 在干什么:Buffer Pool 是"用内存缓存磁盘页",本质是 IO 优化
- < 来自 虚拟内存:让每个程序都以为自己独占一台机器:Buffer Pool 的淘汰用 LRU 变种(改良 LRU),和页面置换同源
- < 来自 调度与并发同步:CPU 分给谁,共享数据谁先动:事务隔离靠锁,锁理论和 OS 完全一致
- > 影响 工程实践:分库分表、读写分离、缓存一致性、分布式事务
自测 3 问
Q1:数据库和文件系统到底差在哪?
点开看答案
文件系统只负责"存",数据库在"存"之上加了四层能力:
- 结构化查询:文件系统只能按路径找;数据库能"找出所有 age > 20 且 city = 北京 的记录",靠索引加速。
- 并发控制:多个进程同时写同一个文件会互相覆盖;数据库用锁和 MVCC 保证正确。
- 事务:文件写到一半断电就坏了;数据库能保证"要么全成,要么全不成"。
- 约束与一致性:主键唯一、外键、非空——文件系统不管这些。
一句话:文件系统是"字节的容器",数据库是"有语义、可并发、可回滚的字节容器"。
Q2:为什么数据库要自己管理缓存(Buffer Pool),不用操作系统的页缓存?
点开看答案
三个原因:
- 可控的淘汰策略。OS 的页缓存用 LRU,但它不知道"哪些页是热数据"。数据库知道自己的访问模式,可以用改良 LRU(比如 InnoDB 的"分区 LRU",防止全表扫描把热数据冲掉)。
- 需要预读和批量。数据库知道"读了索引页,大概率要读数据页",可以主动预读。
- 避免双重缓存。如果既走 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 分钟,中途不卡壳