脏读:一个事务读到了另一个事务尚未提交的修改。
连接1:
连接2:
连接1事务未提交,但是连接2可以读到其修改。
将隔离级别设置为:read commited (读已提交) 可以解决脏读。
不可重复读:同一事务里,两次读同一行,结果不一样。原因是中间有别的事务提交了。
将隔离级别设置为:repeatable read (可重复读) 可以解决不可重复读。
幻读:同一事务里,两次读取同一个范围内的记录得到的结果集不同。当前读和快照读不一样。
在repeatable级别通过加锁解决幻读问题
连接1
连接2:
连接2若想插入,会阻塞等待。当连接1 commit或rollback才会执行。
read uncommited : 脏读、不可重复读、幻读。
read commited : 不可重复读、幻读。
repeatable read : 幻读。
MVCC 是什么
MVCC = Multi-Version Concurrency Control(多版本并发控制)
核心想法:一行数据不只留 当前值,还会保留历史版本。读的时候尽量读某个历史版本,写的时候生成新版本,这样读写常常不用互相锁死。
可以把它想成一行记录有一条 "版本链":
当前版本 (trx=100, balance=300)
↑ undo
旧版本 (trx=80, balance=200)
↑ undo
更旧版本 (trx=50, balance=100)
更新时:写出新版本,旧值放到 undo log,串成链
普通SELECT(快照读):沿版本链往回找,找到「自己可见」的那个版本
SELECT ... FOR UPDATE(当前读):读最新已提交/需要加锁的版本,走锁,不完全靠这套纯快照逻辑。
Read View 是什么
Read View(读视图) 是事务做一致性读时,InnoDB 生成的一份「可见性规则快照」。
它回答的问题只有一个:版本链上这个版本,当前事务能不能看见?
判断逻辑直观理解:
- 版本由已提交且在我视野之前的事务产生 → 可见
- 版本由我自己产生 → 可见
- 版本由生成 Read View 时还未提交的事务产生 → 不可见(继续沿 undo 找更老版本)
- 版本由比我视野更晚的事务产生 → 不可见
找到第一个可见版本,就是这次SELECT的结果。
READ COMMITTED
- 每次普通
SELECT新建一个 Read View - 所以中间别人
COMMIT了,下一次查可能看到新值 - 可能不可重复读、幻读
REPEATABLE READ
- 事务里第一次普通
SELECT创建 Read View,之后复用 - 所以同一事务里多次普通查询,视野不变
- 可重复读;普通
SELECT也很难看到新插入的「幻行」
隔离级别差异:很大程度上就是 Read View 创建时机不同(每次读新建 vs 事务内复用)
在 InnoDB 里,读分两种:
快照读(一致性读)
读的是 Read View 下可见的历史版本,一般不加锁(或几乎不挡别人写)。
常见语句:
SELECT * FROM accounts WHERE id = 1;特点:
- 走 MVCC + Read View
- RR 下同一事务多次读,结果通常一致
- 可能不是磁盘上「此刻最新已提交」的那一版
当前读
读的是 最新版本,并且会加锁,保证读到的行别人不能随便改。
常见语句:
SELECT ... FOR UPDATE; SELECT ... LOCK IN SHARE MODE; -- 或 FOR SHARE UPDATE / DELETE; -- 先当前读再改 INSERT ... SELECT ...; -- 其中的 SELECT 也可能是当前读特点:
- 不靠「复用旧 Read View」来读旧版本
- 要加行锁 / 间隙锁等
- 能读到最新已提交数据(并锁住,防止并发冲突)
MySQL 锁机制详解
MySQL 锁
├── 表级锁:表锁、意向锁、MDL
└── 行级锁(InnoDB 主力)
├── 模式:共享锁 S / 排他锁 X
└── 范围:记录锁 Record / 间隙锁 Gap / 临键锁 Next-Key
行锁的两种模式:S 和 X
| 锁 | 含义 | 常见语句 |
|---|---|---|
共享锁 S | 允许别人再加 S,不允许加 X |
|
排他锁 X | 不允许别人再加 S/X |
|
兼容关系很简单:
- S 与 S:兼容
- S 与 X:不兼容
- X 与 X:不兼容
所以:
- A 持有某行 X 锁时,B 再对同行
FOR UPDATE→ B 等待 - A 只是普通
SELECT(快照读)→ 通常不持有行锁,B 的更新不一定被它堵住
行锁的三种范围:Record / Gap / Next-Key
这是 InnoDB 锁最核心的部分。
1. 记录锁(Record Lock)
锁住已经存在的索引记录。
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;效果:锁住id=1这一行,防止别人改/删它。
2. 间隙锁(Gap Lock)
锁住索引记录之间的空隙,不锁某行本身,但阻止别人往空隙里插入。
例如索引上有id = 10, 20,间隙(10, 20)被锁后:
INSERT INTO t VALUES (15, ...); -- 可能被堵住间隙锁的主要意义:防止幻读(当前读场景下,别插进新行)。
3. 临键锁(Next-Key Lock)
Next-Key Lock = Record Lock + Gap Lock
InnoDB 在 REPEATABLE READ 下,当前读默认常用这套算法:
锁住一个左开右闭区间,例如(10, 20]。
既防止改20,也防止在10和20之间插入新记录。
快照读 vs 当前读
这是理解「锁」和「MVCC」分工的关键。
快照读
SELECT * FROM accounts WHERE id = 1;- 读的是 Read View 下可见的历史版本
- 一般不加行锁
- RR 下同一事务多次普通查询,结果通常稳定
当前读
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; UPDATE accounts SET balance = 200 WHERE id = 1; DELETE FROM accounts WHERE id = 1;- 读最新数据
- 加锁
- 可能阻塞其他事务的冲突写/插
隔离级别如何影响锁
| 隔离级别 | 简要特征 |
|---|---|
READ UNCOMMITTED | 可脏读,基本不用于生产 |
READ COMMITTED | 每次普通 SELECT 新 Read View;当前读主要是记录锁,间隙锁很少 |
REPEATABLE READ(默认) | 快照复用;当前读大量使用 Next-Key,防幻读更强 |
SERIALIZABLE | 普通 SELECT 也会趋向加锁读,并发最差、最严格 |
- 大多数业务用默认 RR 就够
- 若热点行冲突严重、且能接受不可重复读,可评估 RC
- 不要为了“更安全”一上来就 SERIALIZABLE