用费曼学习法,把面试官最想问的 5 个概念一次讲透
undo / redo / binlog —— 三兄弟各司其职,别搞混
MySQL 里跑事务,靠三种日志配合:undo log 记"改之前的值"、redo log 记"改了之后要重放什么"、binlog 记"这个操作是谁发起的"。
把数据库想成一个图书馆:undo log 是"修改记录本"——每次改书之前抄一份原文;redo log 是"施工日志"——记着哪本书哪页要改成什么;binlog 是"借阅流水"——记着每次操作是谁做的,供另一个图书馆(从库)照抄。
redo log 是"引擎内部",用来自己恢复;binlog 是"给外部看",用来复制给从库。职责完全不同,缺一不可——这也正是 2PC 要协调的原因(后面第 6 节讲)。
四个字母,每个字母背后都有具体机制
| 字母 | 含义 | 核心机制 |
|---|---|---|
| A | 原子性 Atomicity | undo log(回滚) |
| C | 一致性 Consistency | 约束 + 业务代码 + 前三性 |
| I | 隔离性 Isolation | 锁(行锁/间隙锁) + MVCC |
| D | 持久性 Durability | redo log + 2PC |
原子性:一份订单要么完全送到,要么完全不送,不会送到一半。
一致性:送之前先检查地址对不对,钱扣了菜没到就是 BUG,业务逻辑要保证。
隔离性:你下单不影响别人下单,各点各的。
持久性:订单一旦确认,就算系统崩了,重启后订单还在。
A · undo log:记录旧值。事务失败按 undo log 反向重放恢复原状。同时参与 MVCC。
C · 业务保证:数据库只保证 A/I/D 三个,一致性靠约束(NOT NULL、UNIQUE、外键)+ 业务代码逻辑正确。
I · 锁 + MVCC:写操作用悲观锁(行锁/间隙锁),读操作用 MVCC 快照读。
D · redo + 2PC:redo log 先刷盘(WAL),配合 2PC 保证 redo 与 binlog 一致。
从 RU 到 Serializable,每升一级多挡一种"读异常"
| 异常 | 现象 | 从哪级解决 |
|---|---|---|
| 脏读 | 读到别人未提交的数据 | RC 起 |
| 不可重复读 | 同一事务两次读,结果不一致 | RR 起 |
| 幻读 | 范围查询看到"新插入的行" | RR 大部分解决 |
RU:你刚看菜单,服务员还在改,你看到的可能不是最终价。
RC:每次问价都拿到最新价,但你问了两次可能不一样。
RR:进入餐厅那一刻菜单就"拍照"了,整个用餐期间看到的价格都是快照的。
Serializable:每次问价都要排队,前面的人问完你才问,价格永远一致但很慢。
| 维度 | RC | RR |
|---|---|---|
| ReadView 生成 | 每次 SELECT 新建 | 事务第一次 SELECT 生成一次 |
| 锁机制 | 只有行锁 | 行锁 + 间隙锁(next-key) |
| 不可重复读 | 存在 | 解决 |
| 幻读 | 存在 | 部分缓解 |
| 性能 | 更高 | 略低 |
多版本并发控制 —— 读不阻塞写、写不阻塞读的魔法
① 行隐藏字段:每行数据偷偷带 DB_TRX_ID(谁改的)、DB_ROLL_PTR(上一版在哪)、DB_DELETE_MARK(是否删除)。
② undo log 版本链:每次 UPDATE 生成新版本,旧版本挂到链上。
③ ReadView:读数据时创建快照视图,用事务 ID 判断哪一版可见。
每次有人修改维基百科词条,都不会覆盖原版,而是新增一个版本。你打开词条时,看到的版本取决于你打开的那一刻"哪些修改已经保存"。MVCC 就是让数据库每行都变成一个"维基百科"——多个版本共存,读者只看到自己该看的那一版。
ReadView = {
m_ids: [在活动时创建 ReadView 的活动事务 ID],
min_trx_id: m_ids 中最小的,
max_trx_id: 下一个要分配的事务 ID (max+1),
creator_trx_id: 创建者自己
}
可见性判定(拿一行数据的 trx_id 对比):
| 条件 | 结果 | 原因 |
|---|---|---|
| trx_id == creator | 可见 | 自己改的 |
| trx_id < min_trx_id | 可见 | 早于所有活动事务,已提交 |
| trx_id ≥ max_trx_id | 不可见 | ReadView 之后才有的 |
| min ≤ trx_id < max,在 m_ids | 不可见 | 在活动列表,可能未提交 |
| min ≤ trx_id < max,不在 m_ids | 可见 | 不在活动列表说明已提交 |
不可见时沿 DB_ROLL_PTR 找上一版继续判断。
这是一次"一致性与性能的折中",不是"性能优先"
四种级别里,RR 是一致性和性能的平衡点:既不太松(会踩坑),也不太严(性能不炸)。默认的哲学是"让用户默认安全,有需求再调"。
保险公司有几种保单,默认给你的不是最便宜也不是最贵的,而是"最常见的中等方案"——大部分场景够用,特殊情况再调整。RR 就是这个中等方案。
如果隔离级别越高越慢,那 RR 就该被淘汰。但 InnoDB 的 MVCC 让 RR 的快照读不加锁,读性能不损失。所以 RR 是"用一致性换锁,不用锁换性能"——这才是 MySQL 敢默认 RR 的底气。
间隙锁死锁多 · 锁范围大 · 锁等待长
只有行锁 · 冲突面小 · 并发高
金融账务 · 强一致业务 · 订单核心
MySQL 内部 2PC 和分布式 2PC 是两回事
协调 redo log 和 binlog,保证两份日志数据一致
协调 多个服务/数据库,让它们一起提交或回滚
拍照前先喊"3、2、1、看镜头",让大家准备好姿势(Prepare);然后一起按快门(Commit)。如果拍照前有人没准备好喊"重来",就整体重来(Abort)。MySQL 内部 2PC 就是这个思路——redo 和 binlog 都是"参与者",InnoDB 是"协调者"。
InnoDB 把 redo log 写入 prepare 状态,但不 commit。此时事务"已准备就绪但没提交"。
MySQL Server 把 binlog 写入并刷盘。binlog 是"提交成功的标志"。
binlog 写完后,InnoDB 把 redo log 从 prepare 改为 commit。事务完成。
| redo 状态 | binlog 状态 | 处理 |
|---|---|---|
| prepare | 未写 | 回滚 |
| prepare | 已写 | 提交 |
| commit | 已写 | 已完成,重放即可 |
因为它是阻塞式协议:协调者挂了参与者一直等、网络抖动就卡住、Prepare 到 Commit 之间有数据不一致窗口。所以现代系统改用 TCC(Try-Confirm-Cancel)、Saga(长事务拆步骤)、Seata AT(无侵入自动补偿)等柔性方案。
undo logredo log + 2PCDB_TRX_ID / DB_ROLL_PTR / DB_DELETE_MARKprepare(不 commit)commit