11.数据库的原理总结
# 数据库的原理总结
10 篇数据库核心原理的精炼。每篇一张速通卡、一段展开、一句话带走。
# 0.阅读导航
# 0.1 按场景读
| 你遇到了什么 | 先读哪篇 |
|---|---|
| 一条 SQL 突然变慢,不知道从哪查起 | → §06 查询优化(EXPLAIN 六步法)→ §02 索引原理(最左前缀) |
| 订单表上千万行后查询越来越慢 | → §09 分库分表(水平拆分)→ §02 索引原理(联合索引) |
| 秒杀系统库存超卖 | → §03 事务隔离(丢失更新)→ §04 锁的实现(乐观锁 vs 悲观锁) |
| 数据库内存打满、Buffer Pool 命中率跳水 | → §05 存储引擎(BP 调优)→ §01 整体架构(Buffer Pool 原理) |
| 主从延迟导致用户看到旧数据 | → §08 主从复制(并行复制)→ §07 日志系统(Binlog) |
| 数据库突然死锁,业务线程僵住 | → §04 锁的实现(死锁检测)→ §03 事务隔离(隔离级别选择) |
| 机房断电后数据能不能恢复 | → §07 日志系统(崩溃恢复)→ §05 存储引擎(Double Write) |
| 想用缓存加速,但不知道缓存怎么配 | → §10 NoSQL 精要(多级缓存)→ §05 存储引擎(BP 预热) |
| 三表 JOIN 全表扫描 | → §06 查询优化(索引改写)→ §02 索引原理(覆盖索引) |
| COUNT(*) 百万行要几十秒 | → §06 查询优化(COUNT 替代)→ §09 分库分表(拆分思路) |
| 面试被问 MySQL 核心原理 | → §01 整体架构 → §02 索引 → §03 事务 → §07 日志 → §08 主从 |
# 0.2 按层次读
第 1 层·架构全景 → §01 整体架构概览
第 2 层·数据引擎 → §05 存储引擎对比 → §10 NoSQL 精要
第 3 层·查询与索引 → §02 索引底层原理 → §06 查询优化精要
第 4 层·并发控制 → §03 事务隔离级别 → §04 锁的实现原理
第 5 层·可靠与持久 → §07 日志系统设计
第 6 层·分布式扩展 → §08 主从复制架构 → §09 分库分表方案
# 1.第 01 篇·数据库整体架构概览
# 1.1 速通卡
| 维度 | 内容 |
|---|---|
| 遇到场景 | 订单列表加载 5 秒——user_id 单列索引扫描 35 万行,加了联合索引反而从 5 秒变 15 秒 |
| 暴露问题 | 不理解 MySQL 的分层架构,加索引凭直觉——范围查询 > 让排序列无法利用索引 |
| 核心知识 | MySQL 分层(Server 层→引擎层)→ SQL 生命周期(连接器→解析器→优化器→执行器→引擎)→ EXPLAIN type 排序 → ACID 实现(undo/redo/MVCC/锁) |
| 架构主线 | Server 层管逻辑、引擎层管存储——一条 SELECT 在 6 个组件间完成接力,每个组件都有性能坑 |
| 加深理解 | EXPLAIN 的 type 字段从左到右排一遍——ALL < index < range < ref < eq_ref < const,你的 SQL 卡在哪一级? |
# 1.2 展开:MySQL 的分层智慧
Server 层五组件接力:连接器(权限快照,FLUSH PRIVILEGES 只对新连接生效)→ 解析器(词法分析→AST 语法树)→ 优化器(CBO 基于成本选索引)→ 执行器(调用引擎 API 取数据)→ 结果返回。
优化器三大常见陷阱:①索引选择不准(Cardinality 统计滞后→ANALYZE TABLE)②JOIN 驱动表选反(小表驱动大表是铁律)③子查询效率差(DEPENDENT SUBQUERY→改 JOIN)。
Buffer Pool 的内部逻辑:16KB 页 × 冷热分离 LRU(3/8 为冷区,5/8 为热区,old_blocks_time=1000ms 防一次性扫描冲刷热数据)。
ACID 一张表对应底层实现:A(undo log 回滚)、I(MVCC+锁)、D(redo log+Double Write)、C(前三者共同保证)。
# 1.3 诊断信号灯
| 信号 | 行动 |
|---|---|
| EXPLAIN type=ALL | 建索引,最差也要到 range |
| Extra=Using filesort | ORDER BY 列加入索引,范围查询放末尾 |
| EXPLAIN key 为空但建了索引 | 检查索引失效(函数/隐式转换/左模糊) |
| 连接数飙升 | 慢查询拖长连接生命周期——先优化最慢的 10 条 SQL |
# 1.4 一句话拎走
MySQL 不是黑盒——一条 SQL 穿过 6 个组件,每个组件都可能让你慢百倍。读懂 EXPLAIN,就拿到了慢查询的 X 光片。
# 2.第 02 篇·数据库索引底层原理
# 2.1 速通卡
| 维度 | 内容 |
|---|---|
| 遇到场景 | 大促 P99 从 50ms 飙升到 5s,用了 idx_user_id 却扫了 35 万行,Extra 出现 Using filesort |
| 暴露问题 | 单列索引只覆盖 user_id,status 和 create_time 的过滤全在回表之后——回表一列 = 一次随机 IO |
| 核心知识 | B+Tree(多叉矮胖、高度=3 可存 80 亿行)→ 聚簇 vs 二级索引 → 最左前缀原则 → 覆盖索引(Using index)→ 十大失效场景 |
| 架构主线 | 索引的本质是有序数据结构——有序让二分查找成为可能,B+Tree 让磁盘随机 IO 变成顺序 IO |
| 加深理解 | EXPLAIN 的 Extra 里有 Using index 吗?——覆盖索引和不覆盖索引能差 500 倍 |
# 2.2 展开:B+Tree 的四个设计智慧
为什么不用二叉树:AVL 树 100 万行需要 20 层,每次磁盘 IO ~10ms,一次查询 200ms+。B+Tree 16KB 页装 1000+ 个键,3 层搞定——3 次磁盘 IO = 30ms。
聚簇索引 vs 二级索引:聚簇索引叶子节点存整行数据(数据即索引)。二级索引叶子节点存主键值 → 必须回表查完整行。回表代价:随机 IO 4-6 次 vs 主键查询 2-3 次。
最左前缀原则:联合索引 (a, b, c) 相当于建了 (a)、(a,b)、(a,b,c) 三个索引。断开条件:遇到范围查询(>, <, BETWEEN),后续列无法使用索引。
十大失效速查:函数运算 / 隐式转换 / 左模糊 LIKE '%xx' / OR 含非索引列 / NOT / != / IS NULL 看字段 / 联合索引不满足最左 / 范围列后的列 / 优化器认为全表更快。
# 2.3 诊断信号灯
| 信号 | 行动 |
|---|---|
| rows 远大于返回行数 | 索引选择性差→重排联合索引列顺序(等值在前、范围在后) |
| Extra 有 Using filesort | ORDER BY 不走索引→排序列加入联合索引 |
| key_len 比自己预想的小 | 最左前缀断了→检查 WHERE 条件顺序 |
| 删了一个索引性能反升 | 优化器之前选错了索引→ANALYZE TABLE 更新统计 |
# 2.4 一句话拎走
索引 = 有序 + 矮胖 + 少回表。B+Tree 解决"有序"和"矮胖",覆盖索引解决"少回表"——掌握这三个词,你就理解了 90% 的索引优化。
# 3.第 03 篇·数据库事务隔离级别
# 3.1 速通卡
| 维度 | 内容 |
|---|---|
| 遇到场景 | 秒杀 999 件商品卖了 1203 件——SELECT stock→判断>0→UPDATE stock-1 两个线程同时读到 999 |
| 暴露问题 | @Transactional 不是魔法——默认 RC 隔离级别下,读写之间的时间窗口就是超卖的温床 |
| 核心知识 | ACID → 四种并发问题(脏读/不可重复读/幻读/丢失更新)→ 四种隔离级别 → MVCC(Read View + undo 版本链)→ 长事务灾难 |
| 架构主线 | 隔离性的本质是并发正确性的程度选择——越隔离越安全但也越慢,MVCC 用快照实现了"安全的并发" |
| 加深理解 | SELECT ... FOR UPDATE 和 SELECT 在 RR 级别下的区别?——前者当前读(读最新+加锁),后者快照读(读事务开始时的版本) |
# 3.2 展开:MVCC 的可见性判断
Read View 的核心字段:m_ids(活跃事务列表)、min_trx_id(最小活跃 ID)、max_trx_id(下一个待分配 ID)、creator_trx_id(自己的事务 ID)。可见性规则:trx_id < min_trx_id → 可见(已提交);trx_id >= max_trx_id → 不可见(未开始);在区间内 → 查 m_ids 列表。
RC vs RR 的 Read View 差异:RC 每次快照读都创建新 Read View(能读到最新提交);RR 只在第一次快照读创建 Read View,事务内一直复用(可重复读)。
长事务三大灾难:①undo log 版本链无限膨胀 ②占用锁不释放 ③从库延迟放大。查出长事务:SELECT * FROM information_schema.INNODB_TRX WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 60。
秒杀修复四方案:①纯乐观锁 WHERE version=? ②悲观锁 SELECT FOR UPDATE ③Redis 预减库存 + DB 兜底 ④Redis + Lua 原子扣减。方案③是高并发场景的最佳实践。
# 3.3 诊断信号灯
| 信号 | 行动 |
|---|---|
| 库存超卖 | 区分快照读→改用当前读或乐观锁 |
| undo 表空间持续增长 | 查长事务 → KILL 或重启 |
| 同一条 SQL 两次结果不一样 | 确认隔离级别→RR 快照读 vs RC 实时读 |
| 事务不报错但数据没变 | 可能被别的事务回滚覆盖→查 binlog |
# 3.4 一句话拎走
事务 = MVCC 快照 + undo 版本链 + 锁 + redo 持久性。Understand Read View, understand everything.
# 4.第 04 篇·数据库锁的实现原理
# 4.1 速通卡
| 维度 | 内容 |
|---|---|
| 遇到场景 | 秒杀系统死锁——事务 A UPDATE WHERE id=10 vs 事务 B UPDATE WHERE id BETWEEN 5 AND 15,范围重叠互相等待 |
| 暴露问题 | InnoDB 不是只有行锁——Record Lock 锁记录、Gap Lock 锁间隙、Next-Key Lock 锁记录+前间隙,RR 下默认 Next-Key Lock |
| 核心知识 | 锁粒度(表/页/行)→ S/X 兼容矩阵 → 三种行锁 → 意向锁 IS/IX → UPDATE 加锁规则 → 死锁四条件 + 检测与避免 |
| 架构主线 | 锁的本质是在索引记录上加锁——无索引的 WHERE 条件 = 全表锁,这是最大的锁事故根源 |
| 加深理解 | UPDATE WHERE id=10 和 UPDATE WHERE name='张三'(name 无索引)加的锁有什么不同?——前者 Record Lock 一行,后者锁全表所有行+间隙 |
# 4.2 展开:InnoDB 的加锁艺术
三种行锁的精确区分:
| 锁类型 | 锁范围 | 冲突对象 | 存在原因 |
|---|---|---|---|
| Record Lock | 索引记录 | 同记录 X 锁 | 行级互斥 |
| Gap Lock | 记录之间的间隙 | Gap 之间不冲突 | 防幻读(INSERT 新行) |
| Next-Key Lock | 记录 + 前间隙 | 等于 Record+Gap | RR 默认锁 |
Gap Lock 之间不冲突:两个事务可以在同一个间隙上加 Gap Lock——因为 Gap 的目的不是互斥而是"禁止插入",两个 Gap 的意图兼容。这是死锁根因:事务 A Gap Lock,事务 B Gap Lock,然后 A 尝试 INSERT→等待 B 释放 Gap,B 尝试 INSERT→等待 A 释放 Gap→死锁。
UPDATE 加锁规则速记:唯一索引等值且记录存在→Record Lock;唯一索引等值但记录不存在→Gap Lock;普通索引等值→Next-Key Lock + 下一个 Gap;范围查询→Next-Key Lock 一路到头;无索引→全表 Next-Key Lock。
死锁避免六法:①固定加锁顺序 ②缩短事务时间 ③用乐观锁替代悲观锁 ④批量操作拆小批次 ⑤RC 隔离级别减少 Gap Lock ⑥重试机制 + 指数退避。
# 4.3 诊断信号灯
| 信号 | 行动 |
|---|---|
SHOW ENGINE INNODB STATUS 有 LATEST DETECTED DEADLOCK | 解读等待图 → 调整加锁顺序 |
| 事务等待超时(Lock wait timeout) | 查 information_schema.INNODB_TRX → 找阻塞源 |
| 简单 UPDATE 极慢 | 大概率 WHERE 条件无索引 → 正在锁全表 |
| RC 级别下死锁比 RR 少 | RC 没有 Gap Lock → 但这意味着可能有幻读 |
# 4.4 一句话拎走
无索引 = 全表锁,有索引 = 精确锁。WHERE 条件的索引覆盖率决定锁粒度——加锁的第一课是给 WHERE 条件建索引。
# 5.第 05 篇·数据库存储引擎对比
# 5.1 速通卡
| 维度 | 内容 |
|---|---|
| 遇到场景 | 大促当晚 Buffer Pool 命中率从 99% 跳水到 75%,P99 从 30ms→800ms,Free buffers 只剩 64 个 |
| 暴露问题 | 缓冲池太小(2GB)被热数据撑爆——Buffer Pool 是 InnoDB 性能的心脏,命中率掉 1% 延迟涨 10 倍 |
| 核心知识 | InnoDB 五层存储架构 → 16KB 页结构 → Buffer Pool(冷热分离 LRU + Free/LRU/Flush 三链表)→ Change Buffer(写加速 5-10 倍)→ Double Write → AHI |
| 架构主线 | InnoDB = B+Tree 的物理实现 + 内存缓冲池的缓存艺术 + 预写日志的崩溃恢复——三者是同一个系统的不同切面 |
| 加深理解 | SHOW ENGINE INNODB STATUS 里 Buffer pool hit rate 是多少?——低于 95% 就该扩容 Buffer Pool 了 |
# 5.2 展开:Buffer Pool 的三个秘密
三链表协作:Free 链表管理空闲页(被取走从 Free 移除),LRU 链表存所有使用中的页(冷热分离 3/8 分界),Flush 链表存脏页(修改过未刷盘的页)。淘汰时从 LRU 冷区尾部取→归还给 Free 链表。
Change Buffer 的写加速:更新的二级索引页不在 Buffer Pool 时,不去磁盘读取——修改缓存到 Change Buffer,等页被读入时再合并。唯一索引不能用(需读盘判重)。全局唯一 vs 普通索引的写入性能可差 5-10 倍。
Double Write 防部分写失效:16KB 页刷盘写到一半断电→页损坏。Double Write 先顺序写 1MB 到共享表空间的 Double Write 区→再随机写到数据文件→如果第二步写到一半断电→恢复时从 Double Write 区还原完整页。
InnoDB vs MyISAM 决定性差异:InnoDB(行锁/事务/CRASH SAFE/B+Tree/聚簇索引),MyISAM(表锁/无事务/崩溃易损坏/B+Tree 叶子存指针/非聚簇)。选 InnoDB 不需要理由,选 MyISAM 需要理由。
# 5.3 诊断信号灯
| 信号 | 行动 |
|---|---|
| BP 命中率 <95% | 增大 innodb_buffer_pool_size |
| Free buffers < 总量的 5% | 同上 + 检查是否有全表扫描冲热数据 |
| 脏页比例 >75% | 调大 innodb_io_capacity 或 innodb_max_dirty_pages_pct |
| 写入 TPS 上不去 | 检查 Change Buffer 是否被唯一索引禁用 |
# 5.4 一句话拎走
InnoDB = 16KB 页的 B+Tree + Buffer Pool 冷热 LRU + Change Buffer 写加速 + Double Write 防损坏。调优四步:看命中率→看空闲页→看脏页→调 io_capacity。
# 6.第 06 篇·数据库查询优化精要
# 6.1 速通卡
| 维度 | 内容 |
|---|---|
| 遇到场景 | 电商首页三表 JOIN 加载 5 秒——全部 type=ALL,rows=289 万行返回仅 20 行 |
| 暴露问题 | 不知道 EXPLAIN 该看哪几个字段——type 和 Extra 是唯二必看的,rows 是第三位 |
| 核心知识 | EXPLAIN 字段解读(type/Extra/rows/key/key_len)→ 慢查询日志 + pt-query-digest → SQL 改写(LIMIT 深分页/COUNT 优化/子查询改 JOIN)→ Optimizer Trace |
| 架构主线 | SQL 优化的本质是让查询尽可能用上索引 + 不回表 = 减少扫描行数 = 减少磁盘 IO |
| 加深理解 | EXPLAIN FORMAT=JSON 比 EXPLAIN 多了什么?——多了具体的代价估算(cost_info)和使用的索引覆盖度(used_columns/attached_condition) |
# 6.2 展开:EXPLAIN 五星级解读
type 从最优到最差:system > const(PK 等值) > eq_ref(JOIN PK) > ref(索引等值) > range(索引范围) > index(全索引扫描) > ALL(全表扫描)。只要 type 是 ALL 或 index,rows 超过返回行数 100 倍,必须优化。
Extra 关键标志位:Using index(覆盖索引,最优!)> Using index condition(ICP 下推,良好)> Using where(过滤,一般)> Using filesort(排序未用索引,差!)> Using temporary(临时表,最差!)。出现最后两项必须修正。
SQL 改写三招:①LIMIT 深分页(OFFSET 1000000→∞)→ 改用基于主键的"上一页最后 ID→下一页"②COUNT(*) 上亿大表→改用 Redis 计数器或估算值 ③子查询 IN (SELECT ...) → 改 JOIN(或 MySQL 5.7+ 自动优化为 semi-join,但不可靠)。
六步优化法:EXPLAIN→看 type 和 Extra→建/改索引→改 SQL→验证 EXPLAIN→压测确认。效果对比:rows 289 万→50,5s→8ms(625 倍提升)。
# 6.3 诊断信号灯
| 信号 | 行动 |
|---|---|
| type=ALL, rows 百万级 | 建联合索引,覆盖 WHERE+ORDER BY |
| Extra=Using temporary | 检查 GROUP BY 和 ORDER BY 是否不同列 |
| Optimizer Trace 显示选择了错误索引 | FORCE INDEX 临时救急 → ANALYZE TABLE 更新统计 |
| 慢查询日志里同一类 SQL 反复出现 | 用 pt-query-digest 聚合→优先优化执行次数最多的 |
# 6.4 一句话拎走
EXPLAIN 两张脸决定一切:type 告诉你怎么找数据,Extra 告诉你找到之后还要做什么。出现 filesort 或 temporary,一定有优化空间。
# 7.第 07 篇·数据库日志系统设计
# 7.1 速通卡
| 维度 | 内容 |
|---|---|
| 遇到场景 | 机房断电后 MySQL 重启——不仅没丢数据,连崩溃前最后几秒提交的事务都在 |
| 暴露问题 | 不知道崩溃恢复的原理——redo 让已提交的不丢,undo 让未提交的不留,binlog 让主从同步不断 |
| 核心知识 | WAL 机制(先写日志再写盘,快 50-100 倍)→ Redo Log(循环写/LSN/Checkpoint)→ Binlog(三种格式/两阶段提交)→ Undo Log(回滚+MVCC)→ 崩溃恢复七步 |
| 架构主线 | 三种日志是一个完整的可靠性三角——redo 管持久、undo 管原子、binlog 管同步 |
| 加深理解 | innodb_flush_log_at_trx_commit=0/1/2 的区别?1=每次提交刷盘(最安全最慢),0=每秒刷(丢 1 秒数据),2=写入 OS 缓存(OS 崩溃丢数据) |
# 7.2 展开:三种日志的协作
WAL(Write-Ahead Logging) 的数学根基:随机写盘寻道 5-10ms,顺序写 redo 日志追加 ~0.1ms——快 50-100 倍。脏页可以延迟刷盘,只要 redo 里有记录,崩溃恢复就能重放。
Redo Log 的 Checkpoint 机制:LSN(Log Sequence Number)是全局物理指针。LSN_checkpoint 之前的日志已安全刷盘可以覆盖。Checkpoint 的作用是告诉恢复系统"从这里开始重放即可"。
Binlog 两阶段提交:①Prepare(写 redo,状态=prepare)②写 binlog ③Commit(redo 状态=commit)。任何一步崩溃后,恢复时检查 binlog 是否完整——binlog 完整则根据 redo commit,binlog 不完整则 redo rollback。这是分布式事务二阶段提交在单机上的经典应用。
UPDATE 一条记录触发三种日志:①查 Buffer Pool,没有则磁盘读入 ②写 undo log(旧值,用于回滚和 MVCC)③修改 Buffer Pool 中的页(变脏页)④写 redo log(新值,prepare 状态)⑤写 binlog ⑥redo log 变 commit 状态→脏页异步刷盘。
# 7.3 诊断信号灯
| 信号 | 行动 |
|---|---|
SHOW ENGINE INNODB STATUS 中 Log sequence 和 Log flushed 差很大 | redo 写压力大,检查 innodb_log_buffer_size |
| Binlog 磁盘使用暴涨 | 可能有未提交的大事务→SHOW BINLOG EVENTS 看大小 |
| 崩溃恢复特别慢 | redo log 太大或 Checkpoint 间隔太长 |
| 从库延迟 | 主库 binlog 量大→开启 ROW 格式+并行复制 |
# 7.4 一句话拎走
Redo: 已提交不丢,Undo: 未提交不留,Binlog: 主从准时。WAL 是整个可靠性设计的基石——先写日志、后写数据,崩溃了从日志重建。
# 8.第 08 篇·数据库主从复制架构
# 8.1 速通卡
| 维度 | 内容 |
|---|---|
| 遇到场景 | 双11支付回调写主库、订单查询走从库——用户看到"支付超时"实际支付成功,主从延迟惹的祸 |
| 暴露问题 | 主从延迟 = 主库写 binlog + 网络传输 + IO 线程写 relay log + SQL 线程重放——四个环节任何一个都可能成为瓶颈 |
| 核心知识 | 三种复制方式(异步/半同步/全同步)→ GTID(自动定位 binlog 位置)→ 并行复制(LOGICAL_CLOCK/WRITESET)→ 读写分离延迟 5 策略 → 高可用架构(MHA/Orchestrator/MGR) |
| 架构主线 | 主从复制的本质是在主库上执行的写操作,在从库上再执行一遍——Binlog 是两者之间的"操作日志" |
| 加深理解 | SHOW SLAVE STATUS 里 Seconds_Behind_Master 为 0 代表什么?——只代表 SQL 线程没有堆积的 relay log,不代表主从数据一致(可能正在复制但没追上) |
# 8.2 展开:从异步到半同步的进化
异步复制的三个致命问题:①主库 binlog 可能还没传到从库就宕机→数据丢失 ②SQL 线程单线程回放→延迟累积 ③GTID 无记录→主从切换需要手动找位点。
半同步复制:主库提交事务时至少等待一个从库确认收到 binlog。AFTER_SYNC(Master 写 binlog→等从库 ACK→提交引擎层,推荐)vs AFTER_COMMIT(先提交再等 ACK,有幻读风险)。从库超时→自动降级为异步→从库恢复后自动升回半同步。
GTID 的三个好处:①自动定位——CHANGE MASTER 不需要 MASTER_LOG_FILE/MASTER_LOG_POS,MySQL 自己知道从哪开始 ②主从切换友好——Orchestrator 故障转移零手动 ③自动跳过已执行事务——RETRIEVED_GTID_SET vs EXECUTED_GTID_SET 查差异。
并行复制:DATABASE(按库,有跨库局限)→ LOGICAL_CLOCK(组提交内的事务可并行,要求 same last_committed)→ WRITESET(行级冲突检测,并行粒度最细,MySQL 8.0 默认)。
读写分离延迟五策略:①读主库(关键业务)②前端规避(同一用户写后读强制走主库)③延迟阈值(seconds_behind_master > 2 不读该从库)④半同步复制 ⑤缓存补偿(把刚写的数据同时写 Redis,读优先走缓存)。
# 8.3 诊断信号灯
| 信号 | 行动 |
|---|---|
| Seconds_Behind_Master 持续增长 | SQL 线程单线程瓶颈→开并行复制+拆分大事务 |
| IO 线程报连接错误 | 网络/防火墙→CHANGE MASTER 重新指定 |
| GTID 集合不一致 | 检查是否有人在从库上直接写了数据(严重禁忌) |
| 从库 CPU 高但主库正常 | SQL 线程回放压力→检查是否有大量 UPDATE/DELETE 大事务 |
# 8.4 一句话拎走
主从复制 = Binlog 传输 + Relay Log 重放 + GTID 自动定位 + 并行回放。延迟是主从架构的永恒问题——读写分离时永远记得"刚写的一定读主库"。
# 9.第 09 篇·数据库分库分表方案
# 9.1 速通卡
| 维度 | 内容 |
|---|---|
| 遇到场景 | 订单表从 500 万涨到 2 亿行,单表查询 200ms,COUNT(*) 30 秒——B+Tree 维护成本指数级增长 |
| 暴露问题 | 单表 2000 万行后,即使有索引,页分裂和 Buffer Pool 命中率下降让性能崩塌——水平分表是唯一的破局方式 |
| 核心知识 | 垂直拆分 vs 水平拆分 → 分片键选择三铁律 → 分片算法(取模/范围/一致性哈希)→ 雪花算法分布式 ID → 双写零停机迁移 → 中间件(ShardingSphere) |
| 架构主线 | 分库分表 = 分片键 + 分片算法 + 分布式 ID + 路由中间件 + 灰度迁移——五件少任何一件都是灾难 |
| 加深理解 | WHERE user_id = 123 能精准路由到 1 张分表,WHERE create_time > '2024-01-01' 却要广播 32 张表——分片键决定了哪些查询能精准路由 |
# 9.2 展开:拆分的五个关键决策
分片键三铁律:①绝大多数查询必须带这个字段(不带=全分片广播)②数据分布尽量均匀(热点分片=木桶的短板)③不可变(一旦变了=数据迁移)。用户系统的分片键首选 user_id,订单系统的分片键首选 user_id 或 order_id。
雪花算法 64bit 结构:1bit 保留 + 41bit 毫秒时间戳(可用 69 年)+ 10bit 机器 ID(1024 台)+ 12bit 序列号(每毫秒 4096 个)。单机理论 TPS=409.6 万/秒。时钟回拨问题:等时钟恢复 + 用历史最大序列号 + 备用机器 ID。
双写零停机迁移四阶段:①存量数据全量迁移(不停止旧服务)②增量双写(新数据同时写新旧两套分片,中间件记录差异日志)③对比校验(定时对账,补偿差异)④灰度切读(先 1%→10%→50%→100%),切读确认无误后停止双写旧库。
跨分片查询三大思路:①精准路由(WHERE 带分片键→只查 1 张表,最快)②全分片广播(无分片键→查所有表→合并排序,代价大)③冗余索引表(另建一张 order_id→user_id 映射表,通过它找到分片键再精准查询)。
# 9.3 诊断信号灯
| 信号 | 行动 |
|---|---|
| 单表 >2000 万行 | 开始规划水平拆分 |
| 某些分片数据是其他的 5 倍 | 分片键数据倾斜→换分片算法或分片键 |
| 跨分片 JOIN 卡死 | 拆成多次查 + 应用层组装 |
| 迁移后旧数据没有被读到 | 灰度比例验证 → 缓存 100% 旧数据+对比 |
# 9.4 一句话拎走
分库分表不是银弹——它把"垂直能力"的瓶颈变成了"选对分片键+扛住迁移复杂性"的能力。决策树:能不分→只拆分→必须水平→双写迁移→灰度切换。
# 10.第 10 篇·NoSQL 设计精要
# 10.1 速通卡
| 维度 | 内容 |
|---|---|
| 遇到场景 | 双11首页秒杀商品列表 QPS 飙到 5 万,MySQL 从库 CPU 100%——每秒 2 万次执行同一条 SELECT,读写分离没解决重复计算 |
| 暴露问题 | 2 万次查询从库仍执行了 2 万次磁盘 IO——缓存层缺失让数据库反复算同样的结果 |
| 核心知识 | CAP 定理 → Redis 五大数据结构(底层编码自动切换)→ 持久化(RDB/AOF/混合)→ 淘汰策略 8 种 → 高可用(Sentinel/Cluster 16384 槽)→ 多级缓存架构 |
| 架构主线 | NoSQL 不是替代 MySQL——SQL 负责强一致+复杂查询,NoSQL 负责高性能+灵活模型,两者互补 |
| 加深理解 | Redis 单线程为什么快?——所有数据在内存(纳秒级)+ 单线程无锁竞争 + epoll 多路复用(非阻塞)——快的本质是不等磁盘、不等锁 |
# 10.2 展开:Redis 的五个关键设计
底层编码自动切换:小数据用紧凑结构(ziplist/intset/listpack),大数据切换到复杂结构(skiplist/hashtable/quicklist)。比如 ZSET 在元素 <128 且每个元素 <64 字节时用 ziplist,否则转 skiplist+hashtable。这就是"小数据零内存浪费,大数据高性能查询"的自适应设计。
RDB vs AOF:RDB(快照,全量,适合备份恢复)vs AOF(命令日志,增量,丢失少但文件大恢复慢)。混合持久化(Redis 4.0+):RDB 存基准快照 + AOF 存增量命令,兼具恢复速度和数据完整性。
8 种淘汰策略速记:noeviction(满了直接报错)→ allkeys-lru(最常用,全局淘汰最少使用)→ volatile-lru(只淘汰设了 TTL 的)→ allkeys-lfu(全局淘汰最不频繁使用)→ volatile-lfu → allkeys-random → volatile-random → volatile-ttl(淘汰即将过期的)。生产环境首选 allkeys-lru。
COW(Copy-On-Write)持久化原理:fork() 创建子进程做 RDB,父子共享相同的物理内存页(页表复制,不复制数据)。父进程修改某页时,OS 才为该页创建副本——fork 瞬间快,但修改量大时 COW 内存翻倍是 fork 崩溃的主因。
缓存三大问题及解法:
| 问题 | 现象 | 解决方案 |
|---|---|---|
| 缓存穿透 | 查不存在的数据,缓存没命中,每次都打 DB | 布隆过滤器 + 缓存空值 |
| 缓存击穿 | 某个热点 Key 过期瞬间大量请求打 DB | 互斥锁 + 逻辑过期(不等重建,异步刷新) |
| 缓存雪崩 | 大量 Key 同时过期,DB 瞬间被打垮 | 过期时间加随机值 + 多级缓存 + 限流熔断 |
# 10.3 诊断信号灯
| 信号 | 行动 |
|---|---|
| RDB fork 时内存翻倍 OOM | 预留 maxmemory 的 50% 空闲内存 |
| AOF 文件比 RDB 大 3 倍+ | 开启 AOF 重写(BGREWRITEAOF) |
| 缓存大量 miss 且 DB 没挂 | 检查 key 是否被误 expire→TTL key |
| 主从断连重连后全量同步 | 调整 repl-backlog-size 避免全量同步 |
# 10.4 一句话拎走
Redis 快不是因为它是内存数据库,而是因为单线程 epoll + 精心设计的数据结构让每次操作都在纳秒级完成。缓存设计四原则:越近越好、越热越缓存、设好 TTL、兜底永不忘。
# 11.全系列·贯通三句话
# 11.1 数据走多远决定性能——I/O 是数据库的阿喀琉斯之踵
内存访问(100ns) → Buffer Pool命中(不用磁盘) → Change Buffer 写加速
磁盘顺序读(0.1ms) → Redo顺序写 → Binlog顺序写
磁盘随机读(5-10ms) → 回表 → 全表扫描 → 索引失效
所有数据库优化的底层逻辑:让磁盘 I/O 尽可能少、尽可能顺序。
# 11.2 有序是索引的一切——B+Tree 的设计哲学
| 问题 | B+Tree 的答案 |
|---|---|
| 磁盘 I/O 太慢 | 多叉→3 层装下 80 亿行,3 次 I/O 完成查询 |
| 范围查询要扫多条记录 | 叶子节点双向链表→一次定位、顺序扫描 |
| 数据变动需要快速更新 | 页分裂/页合并→局部调整,不重构整棵树 |
| 回表太慢 | 覆盖索引→索引叶子直接包含查询列,零回表 |
理解 B+Tree 的三个关键数字:16KB 页大小、3 层高度 = 80 亿行、一次磁盘 I/O = 10ms。
# 11.3 日志是数据库的生命线——WAL 统一了性能与可靠性
| 日志 | 写什么 | 写入方式 | 解决了什么问题 |
|---|---|---|---|
| Redo Log | 物理页修改 | 顺序、循环写 | 崩溃后恢复已提交事务的数据 |
| Undo Log | 数据旧版本 | 追加写 | 事务回滚 + MVCC 多版本快照 |
| Binlog | 逻辑 SQL/行变更 | 顺序、追加写 | 主从复制 + 数据恢复 |
三者的协作:一次 UPDATE 触发 Redo(崩溃恢复) + Undo(快照读) + Binlog(主从同步),两阶段提交保证三者一致。
# 12.全系列数据一页纸
| 篇 | 核心数据 | 含义 |
|---|---|---|
| 01 | 6 组件 + 5 层 | MySQL 分层架构(Server×5 + 引擎×1) |
| 02 | 3 层 80 亿 | B+Tree 高度 3 可装 80 亿行;覆盖索引快回表 500 倍 |
| 03 | 4 种问题 + 4 级 | 并发问题矩阵 × 隔离级别选择 |
| 04 | 3 种行锁 + 4 维矩阵 | Record/Gap/Next-Key × IS/IX/S/X 兼容性 |
| 05 | 16KB + 3 链表 | 页大小 + Free/LRU/Flush 三链表协作 |
| 06 | type 7 级 + Extra 6 种 | EXPLAIN 字段优先级(type > Extra > rows) |
| 07 | 3 种日志 + 2 阶段 | redo/binlog/undo × 两阶段提交 |
| 08 | 3 种复制 + GTID | 异步/半同步/全同步 × 自动定位 |
| 09 | 2^32 个 ID | 雪花算法 64bit=41 时间+10 机器+12 序列 |
| 10 | 8 种策略 + 3 大问题 | 淘汰策略 × 缓存穿透/击穿/雪崩 |
# 13.全系列症状诊断表
对照症状,直接跳到对应篇章。
| 你看到的症状 | 第一反应读这篇 | 第二篇 |
|---|---|---|
| SQL 变慢,EXPLAIN type=ALL | §06 查询优化 | §02 索引原理 |
| ORDER BY 慢,Extra=filesort | §06 EXPLAIN Extra | §02 联合索引最左前缀 |
| 库存超卖、对账不平 | §03 事务隔离 | §04 锁的实现 |
| 数据库死锁日志 | §04 死锁检测 | §03 隔离级别改为 RC |
| BP 命中率 <95%,P99 飙升 | §05 Buffer Pool 调优 | §01 整体架构 |
| 主从延迟,读从库拿到旧数据 | §08 复制延迟 | §07 日志系统 |
| 表超过 2000 万行,COUNT 要几十秒 | §09 分库分表 | §02 索引覆盖 |
| 慢查询日志爆炸 | §06 pt-query-digest | §02 索引失效 |
| 崩溃恢复后数据丢了 | §07 Redo/Binlog | §05 Double Write |
| 缓存加了 QPS 没降 | §10 多级缓存架构 | §05 Buffer Pool 替代缓存层 |
| 大事务执行一半被 KILL | §03 长事务 | §07 Undo 膨胀 |
| 写入 TPS 低 | §05 Change Buffer | §02 唯一索引限制 |
| UPDATE 几行数据特别慢 | §04 索引与加锁 | §02 二级索引加锁规则 |
# 14.学习路径建议
# 14.1 半天速通
读 §0 导航 → 每篇只看 速通卡 + 一句话拎走(每篇 2 分钟,10 篇共 20 分钟)。遇到具体场景再展开。
# 14.2 系统建立数据库直觉(推荐)
§01 整体架构(SQL 生命周期) → §02 索引原理(B+Tree+覆盖)
→ §06 查询优化(EXPLAIN 六步法)
→ §03 事务隔离(MVCC+四种级别) → §04 锁的实现(三种行锁+死锁)
→ §05 存储引擎(Buffer Pool+InnoDB 架构)
→ §07 日志系统(三种日志+崩溃恢复)
→ §08 主从复制(并行复制+延迟处理)
→ §09 分库分表(水平拆分+双写迁移)
→ §10 NoSQL(Redis+多级缓存)
# 14.3 排查问题时
看 §13 诊断表 定位篇章 → 看速通卡确认方向 → 看信号灯验证假设 → 看展开加深理解。
# 14.4 自测清单
读完 10 篇后闭卷回答:
- MySQL 一条
SELECT经过了哪些组件?每个组件的核心职责是什么? - B+Tree 为什么用多叉而不是二叉树?3 层能装多少行数据?
- 联合索引的最左前缀原则是什么?范围查询为什么让后续列失效?
- MVCC 的 Read View 怎么判断一行数据是否可见?RC 和 RR 的 Read View 创建时机有何不同?
- Record Lock / Gap Lock / Next-Key Lock 三者各自锁什么?无索引 WHERE 条件加什么锁?
- Buffer Pool 的三个链表分别管什么?冷热分离 LRU 的分界线在哪?
- Redo Log / Undo Log / Binlog 各自解决什么问题?两阶段提交确保了什么?
- 主从延迟的四个来源是什么?读写分离怎么应对延迟?
- 分片键选择的三个铁律是什么?双写迁移的四个阶段是什么?
- 缓存的三大问题(穿透/击穿/雪崩)分别怎么解决?