在上一篇博客中,我们已经学习了:
-
事务的 ACID 特性
-
四种隔离级别
-
Read Uncommitted
-
Read Committed
并且知道:
Read Committed 会出现"不可重复读"问题
而 MySQL 默认隔离级别:
REPEATABLE READ
却能解决这个问题。
那么:
-
MySQL 是怎么做到的?
-
为什么普通 SELECT 不加锁也能保证一致性?
-
MVCC 到底是什么?
-
Read View 是什么?
-
幻读为什么难解决?
这一篇,我们彻底搞懂。
一、Repeatable Read(可重复读)
Repeatable Read:
可重复读
简称:
RR
这是:
-
MySQL默认隔离级别
-
面试高频核心
-
MVCC最重要的应用场景
RR 的核心目标
保证:
同一个事务中,多次读取同一数据,结果必须一致
二、Read Committed 为什么不行?
在 Read Committed 下:
事务A:
BEGIN;
SELECT balance FROM account WHERE id = 1;
结果:
100
事务B:
UPDATE account SET balance = 200 WHERE id = 1;
COMMIT;
事务A再次读取:
SELECT balance FROM account WHERE id = 1;
结果:
200
同一个事务:
两次读取结果不同。
这就是:
不可重复读
原因
因为:
RC 每次 SELECT 都读取"最新已提交数据"。
所以:
别人提交之后:
当前事务立刻就能看到。
三、Repeatable Read 如何解决?
RR 的核心思想:
第一次读取时,生成一个"数据快照"。
之后:
-
不再读取最新数据
-
而是读取事务开始时看到的数据
因此:
即使别人修改并提交:
当前事务依然读取旧值。
四、RR实验演示
事务A
BEGIN;
SELECT balance FROM account WHERE id = 1;
结果:
100
事务B
UPDATE account SET balance = 200 WHERE id = 1;
COMMIT;
事务A再次读取
SELECT balance FROM account WHERE id = 1;
结果仍然:
100
为什么?
因为:
RR读取的不是最新数据。
而是:
"事务开始时的数据快照"
五、MVCC(多版本并发控制)
RR 能实现的核心:
MVCC
全称:
Multi-Version Concurrency Control
即:
多版本并发控制
六、为什么需要 MVCC?
假设数据库只有一份数据:
balance = 100
事务A正在读取。
此时事务B修改:
balance = 200
那么:
事务A就无法继续读取旧值。
于是:
MySQL想到一个办法:
不直接覆盖旧数据。
而是:
保留多个版本
七、MVCC 的核心思想
假设:
最开始:
100
之后有人修改:
200
再修改:
300
实际上:
数据库内部更像:
300
↓
200
↓
100
形成:
版本链
不同事务:
读取不同版本。
八、MVCC 的优点
1. 普通SELECT不加锁
例如:
SELECT * FROM account;
不会阻塞写操作。
2. 写操作也不阻塞读
相比传统:
读加锁
写加锁
MVCC并发能力高很多。
3. 实现可重复读
事务始终读取:
自己事务开始时的版本。
九、隐藏字段
InnoDB 每一行数据后面:
实际上隐藏了几个字段。
虽然我们平时看不到。
但它们非常重要。
1. trx_id
表示:
最后一次修改该行的事务ID
例如:
事务10修改了该行
那么:
trx_id = 10
2. roll_pointer
指向:undo log
即:
旧版本数据。
十、undo log(回滚日志)
undo log:回滚日志
是MVCC真正保存历史版本的位置。
十一、undo log 的作用
1. 事务回滚
例如:
ROLLBACK;
数据库需要恢复旧值。
2. MVCC读取历史版本
事务读取快照时:
实际上会:
沿着undo log查找旧版本
十二、版本链
假设:
数据经历:
100 -> 200 -> 300
内部实际上:
300
↓
200
↓
100
通过:
roll_pointer
串起来。
这就形成:
版本链
十三、Read View(读视图)
MVCC 中最核心的概念:
Read View
一、什么是 Read View?
可以理解为:
当前事务观察数据库的"视角"。
二、什么时候生成?
在 RR 下:
第一次 SELECT 时生成。
之后:
整个事务复用同一个 Read View。
三、作用
决定:
哪些版本当前事务可以看到
十四、Read View 的简单理解
事务启动时:
数据库会记录:
当前有哪些事务正在活跃
之后判断:
某个数据版本是否对当前事务可见
举个简单例子
假设:
当前:
事务5正在运行
事务6正在运行
事务7正在运行
事务8开始读取。
那么:
Read View 会记录:
[5,6,7]
之后:
如果某数据是:
事务6修改的
但事务6还没提交。
那么:
事务8不能看到。
十五、快照读 与 当前读
1. 快照读(Snapshot Read)
普通 SELECT:
SELECT * FROM account;
特点:
-
使用MVCC
-
不加锁
-
读取历史快照
2. 当前读(Current Read)
读取最新数据:
并加锁。
例如:
SELECT * FROM account FOR UPDATE;
或者:
UPDATE ...
DELETE ...
INSERT ...
这些都属于:
当前读
当前读特点
-
读取最新版本
-
会加锁
-
可能阻塞
十六、为什么 RR 能读到自己修改的数据?
这是很多人第一次学MVCC时最困惑的点。
事务A:
BEGIN;
SELECT balance FROM account WHERE id = 1;
结果:
100
事务A自己修改:
UPDATE account SET balance = 500 WHERE id = 1;
再次查询:
SELECT balance FROM account WHERE id = 1;
结果:
500
不是说:
RR读取旧快照吗?
为什么又变成500了?
原因
因为:
事务永远能看到自己修改的数据。
MySQL 会特殊处理:
自己写的数据,对自己始终可见
十七、幻读(Phantom Read)
幻读:
同一个事务中,两次查询的数据"数量"不一致。
示例
事务A:
BEGIN;
SELECT * FROM student WHERE age = 18;
结果:
3条数据
事务B:
INSERT INTO student VALUES(...,18);
COMMIT;
事务A再次查询:
SELECT * FROM student WHERE age = 18;
结果:
4条数据
像突然"冒出来"一条。
因此:
幻读
十八、MySQL 如何解决幻读?
理论上:
RR 不能完全解决幻读。
但是:
InnoDB 对幻读做了很强的优化。
核心:
Next-Key Lock(临键锁)
十九、Next-Key Lock(临键锁)
本质:
行锁 + 间隙锁
不仅锁当前行。
还锁:
数据之间的间隙
为什么锁间隙?
为了防止:
别人插入新数据
从而避免幻读。
二十、Serializable(串行化)
最高隔离级别:
SERIALIZABLE
特点
所有事务:
排队执行
类似:
单线程
优点
不会出现:
-
脏读
-
不可重复读
-
幻读
缺点
性能最差。
并发能力最低。
因此:
生产环境极少使用。
二十一、四种隔离级别总结
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| Read Uncommitted | 会 | 会 | 会 |
| Read Committed | 不会 | 会 | 会 |
| Repeatable Read | 不会 | 不会 | 基本不会 |
| Serializable | 不会 | 不会 | 不会 |
二十二、为什么 MySQL 默认选择 RR?
原因:
1. 一致性更强
相比RC:
-
避免不可重复读
-
数据更稳定
2. MVCC性能优秀
虽然隔离更强。
但是:
普通SELECT不加锁
因此性能仍然很高。
3. InnoDB优化了幻读问题
通过:
-
MVCC
-
Next-Key Lock
实现:
高隔离 + 高性能
二十三、事务底层原理总结
事务底层最核心:
一、原子性
通过:
undo log
实现回滚。
二、持久性
通过:
redo log
保证崩溃不丢数据。
三、隔离性
通过:
-
锁
-
MVCC
实现。
四、一致性
由:
- ACID共同保证
最终实现。
二十四、总结
事务真正解决的问题:
高并发下的数据一致性
MySQL通过:
-
undo log
-
redo log
-
MVCC
-
Read View
-
Next-Key Lock
-
锁机制
最终实现:
高并发下的数据安全与高性能