第一部分:存储引擎与索引结构
1.1 什么是存储引擎?
MySQL 支持多种存储引擎 (Storage Engine),负责数据的存储、检索和事务管理。
最常用的是 InnoDB(默认,支持事务、行锁、外键)和 MyISAM(不支持事务,表锁)。
✅ 生产环境几乎全部使用 InnoDB。
1.2 InnoDB 的数据组织:B+ 树
🔍 为什么用 B+ 树?
- 磁盘 I/O 是性能瓶颈 → 需要减少磁盘读取次数
- B+ 树是多路平衡搜索树,高度低(通常 3~4 层),一次查询只需 3~4 次磁盘 I/O
🌲 B+ 树 vs B 树
| 特性 | B 树 | B+ 树(InnoDB 使用) |
|---|---|---|
| 数据存储位置 | 内部节点 + 叶子节点 | 仅叶子节点存数据 |
| 叶子节点连接 | 无 | 双向链表连接(利于范围查询) |
| 查询效率 | 稳定 | 更稳定 + 范围扫描快 |
📂 InnoDB 的两类 B+ 树索引
| 类型 | 说明 | 特点 |
|---|---|---|
| 聚簇索引(Clustered Index) | 主键索引,叶子节点存整行数据 | 表数据按主键物理排序 |
| 二级索引(Secondary Index) | 普通索引,叶子节点存主键值 | 查非主键字段需"回表" |
💡 例:
SELECT name FROM users WHERE id = 100;
id是主键 → 走聚簇索引,1 次 I/O- 若查
email = 'a@b.com',而id,再回表查聚簇索引 → 2 次 I/O
第二部分:事务与 ACID
2.1 什么是事务(Transaction)?
事务是一组数据库操作,要么全部成功,要么全部失败,是一个不可分割的工作单元。
sql
BEGIN;
UPDATE account SET balance = balance - 100 WHERE user = 'A';
UPDATE account SET balance = balance + 100 WHERE user = 'B';
COMMIT; -- 或 ROLLBACK
2.2 ACID 四大特性
| 特性 | 含义 | MySQL 如何实现 |
|---|---|---|
| Atomicity(原子性) | 事务不可分割 | Undo Log(回滚日志) |
| Consistency(一致性) | 事务前后数据合法(如余额 ≥0) | 应用逻辑 + 约束(唯一索引等) |
| Isolation(隔离性) | 并发事务互不干扰 | MVCC + 锁机制 |
| Durability(持久性) | 提交后数据永久保存 | Redo Log + innodb_flush_log_at_trx_commit=1 |
2.3 隔离级别与并发问题
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | MySQL 默认 |
|---|---|---|---|---|
| Read Uncommitted | ✅ | ✅ | ✅ | ❌ |
| Read Committed | ❌ | ✅ | ✅ | (Oracle 默认) |
| Repeatable Read | ❌ | ❌ | ⚠️(基本解决) | ✅ InnoDB 默认 |
| Serializable | ❌ | ❌ | ❌ | 性能差 |
🔍 InnoDB 在 RR 级别通过 MVCC + Next-Key Lock 解决幻读
三大并发问题:
- 脏读:读到未提交的数据
- 不可重复读:同一事务中多次读,结果不同(行被 update)
- 幻读:同一事务中多次查,行数不同(有新行 insert)
第三部分:日志系统 ------ 崩溃恢复的核心
InnoDB 通过 WAL(Write-Ahead Logging) 机制保证性能与安全:
3.1 Redo Log(重做日志)
- 作用 :崩溃恢复时重放已提交事务
- 特点 :
- 物理日志(记录"页修改")
- 循环写入(固定大小,如 4GB)
- 先写 Redo Log,再改内存数据页
💡
innodb_flush_log_at_trx_commit = 1控制 Redo Log 刷盘时机
3.2 Undo Log(回滚日志)
- 作用 :
- 事务回滚
- MVCC(多版本并发控制)提供历史快照
- 存储:在共享表空间或独立 undo 表空间
3.3 Binlog(二进制日志)
- 作用 :
- 主从复制
- 基于时间点的数据恢复
- 格式 :
STATEMENT:记录 SQL(可能不一致)ROW:记录每行变更(推荐)MIXED:混合
💡
sync_binlog = 1控制 Binlog 刷盘时机
第四部分:主从复制原理
4.1 复制流程(异步)
Slave_DB Slave_SQL_Thread Relay_Log Slave_IO_Thread Master Slave_DB Slave_SQL_Thread Relay_Log Slave_IO_Thread Master 1. 写 Binlog 2. Slave 连接,请求 Binlog 3. 写 Relay Log 4. 重放 Relay Log
4.2 GTID(全局事务 ID)
- 格式:
server_uuid:transaction_id - 优势:自动定位复制位点,切换主从无需找
binlog_file + position
第五部分:高可用架构(无 MGR/MHA)
5.1 架构选择:双主(Active-Standby) + 多从
- 只允许一个主写,避免冲突
- 备主平时
read_only = ON - 用
auto_increment奇偶分离防主键冲突
5.2 关键配置(所有节点)
ini
# 安全底线
innodb_flush_log_at_trx_commit = 1
sync_binlog = 1
# 强一致性基础
gtid_mode = ON
enforce_gtid_consistency = ON
binlog_format = ROW
# 防冲突
auto_increment_increment = 2
auto_increment_offset = 1 # 主库=1,备库=2
5.3 手动故障切换步骤
- 停止备库复制:
STOP SLAVE; RESET SLAVE ALL; - 解除只读:
SET GLOBAL read_only = OFF; - 漂移 VIP 或更新应用配置
- 修复原主后,作为新从库加入
第六部分:Redis 补充(缓存与加速)
6.1 Redis 核心价值
- 缓存:减轻 MySQL 读压力
- 原子操作 :
INCR/DECR实现库存扣减 - 数据结构:List(队列)、ZSet(排行榜)
6.2 秒杀场景示例
python
stock_key = "stock:1001"
new_stock = r.decr(stock_key)
if new_stock >= 0:
# 创建订单
else:
r.incr(stock_key) # 补偿!
raise HTTPException(429, "售罄")
✅
INCR/DECR是 Redis 原子命令,线程安全
第七部分:总结 ------ 生产 Checklist
| 类别 | 必做项 |
|---|---|
| 存储 | 使用 InnoDB,主键自增 |
| 索引 | 合理设计二级索引,避免回表 |
| 事务 | 用 RR 隔离级别,短事务 |
| 日志 | 双1配置 + ROW 格式 binlog |
| 复制 | GTID + 半同步(可选) |
| 高可用 | 双主单写 + VIP + 监控 |
| 缓存 | Redis 缓存热点数据,防穿透/击穿 |
🌟 记住 :
数据库不是黑盒。理解 B+ 树、事务、日志、复制原理,才能在高并发、高可用场景下做出正确决策。
非常好的问题!MVCC(Multi-Version Concurrency Control,多版本并发控制) 是现代数据库(如 MySQL InnoDB、PostgreSQL、Oracle)实现高并发读写 的核心机制。它让你在不加锁 的情况下,也能安全地进行快照读(Snapshot Read),极大提升系统吞吐量。
下面我们用 通俗语言 + 技术细节 + 示例 来彻底讲清楚 MVCC。
一、为什么需要 MVCC?
🚫 传统锁机制的问题
假设没有 MVCC,要保证事务隔离性,只能靠加锁:
- 读操作 → 加共享锁(S锁)
- 写操作 → 加排他锁(X锁)
后果:
- 一个长事务在更新某行,其他所有读请求都会被阻塞!
- 系统并发能力极低,用户体验差。
✅ MVCC 的目标
读不阻塞写,写不阻塞读!
即使有事务在修改数据,其他事务仍能看到一致的历史版本,无需等待。
二、MVCC 的核心思想
"不是直接修改数据,而是保留多个版本,每个事务看到自己该看的版本。"
就像 Git 的分支:
- 每次修改生成一个新 commit(新版本)
- 不同人可以基于不同 commit 工作,互不影响
三、InnoDB 如何实现 MVCC?
InnoDB 通过以下 3 个关键技术 实现 MVCC:
1️⃣ 隐藏字段(每行记录自带)
InnoDB 在每行数据中隐式添加 3 个字段(用户不可见):
| 字段 | 说明 |
|---|---|
DB_TRX_ID |
最后修改该行的事务 ID |
DB_ROLL_PTR |
回滚指针,指向 Undo Log 中的旧版本 |
DB_ROW_ID |
行 ID(仅当表无主键时使用) |
💡 事务 ID(trx_id)是全局递增的数字,启动时分配。
2️⃣ Undo Log(版本链)
- 每次 UPDATE/DELETE 时,不直接覆盖原数据 ,而是:
- 将旧行复制到 Undo Log
- 新行的
DB_ROLL_PTR指向这个旧版本
- 多次修改形成 版本链(Version Chain)
text
当前行 (trx_id=105)
↑
| DB_ROLL_PTR
↓
旧版本 (trx_id=102)
↑
| DB_ROLL_PTR
↓
更旧版本 (trx_id=100)
3️⃣ Read View(读视图)
当一个事务执行快照读 (如普通 SELECT)时,InnoDB 会创建一个 Read View ,决定"能看到哪些版本"。
Read View 包含 4 个关键信息:
| 成员 | 说明 |
|---|---|
m_ids |
当前活跃事务 ID 列表(未提交的事务) |
min_trx_id |
m_ids 中最小的事务 ID |
max_trx_id |
创建 Read View 时,下一个将分配的 trx_id |
creator_trx_id |
当前事务自己的 ID(若为读事务则为 0) |
四、可见性判断规则(核心!)
给定一行数据(版本),当前事务能否看到它?按顺序判断:
-
如果行的
DB_TRX_ID== 当前事务 ID→ 自己改的,可见
-
如果行的
DB_TRX_ID<min_trx_id→ 该行在 Read View 创建前已提交,可见
-
如果行的
DB_TRX_ID≥max_trx_id→ 该行在 Read View 创建后才开启,不可见
-
如果
DB_TRX_ID在[min_trx_id, max_trx_id)区间内→ 检查是否在
m_ids(活跃事务列表)中:- 在 → 未提交,不可见
- 不在 → 已提交,可见
-
如果不可见,就沿着
DB_ROLL_PTR找上一个版本,重复判断
✅ 这就是"一致性非锁定读"的原理!
五、举个完整例子 🌰
场景:账户余额查询
| 时间 | 事务 A(trx_id=100) | 事务 B(trx_id=101) |
|---|---|---|
| T1 | BEGIN; |
|
| T2 | SELECT balance FROM account WHERE id=1; → 看到 1000 |
|
| T3 | BEGIN; |
|
| T4 | UPDATE account SET balance = 800 WHERE id=1; |
|
| T5 | SELECT balance FROM account WHERE id=1; → 仍看到 1000! |
|
| T6 | COMMIT; |
|
| T7 | SELECT balance FROM account WHERE id=1; → 看到 800 |
分析:
- T2 时,事务 A 创建 Read View:
m_ids=[100],min=100,max=102 - T5 时,事务 A 再次 SELECT:
- 当前行
DB_TRX_ID=101(事务 B 修改的) 101在[100, 102)且在m_ids?→ 事务 B 未提交(T5 时还在运行) → 不可见- 沿着 Undo Log 找到旧版本:
DB_TRX_ID=100→ 是自己事务 → 可见!值为 1000
- 当前行
✅ 事务 A 始终看到事务开始时的一致快照,不受其他未提交事务影响。
六、MVCC 与隔离级别的关系
| 隔离级别 | 是否使用 MVCC? | 行为 |
|---|---|---|
| Read Committed (RC) | ✅ | 每次 SELECT 都创建新 Read View → 能看到其他事务已提交的新数据 |
| Repeatable Read (RR) | ✅ | 事务中第一次 SELECT 创建 Read View,后续复用 → 保证可重复读 |
| Serializable | ❌ | 强制加锁,退化为串行执行 |
🔍 MySQL InnoDB 默认 RR 级别,正是靠 MVCC 实现"幻读基本解决"
七、MVCC 的优势与代价
✅ 优势
- 高并发:读写不互相阻塞
- 一致性:提供事务开始时的数据快照
- 性能好:避免大量读锁开销
⚠️ 代价
- 存储开销:Undo Log 占用额外空间
- 清理延迟:旧版本不能立即删除(需等所有可能用到它的事务结束)
- 长事务危害:会导致 Undo Log 无法 purge,磁盘爆满!