MySQL InnoDB 四种事务隔离级别实现原理
InnoDB 隔离级别基于MVCC (多版本并发控制)+锁(行锁、Gap 临键锁)共同实现; MVCC 主要解决读不加锁(快照读);锁用来解决当前读的幻读问题。 SQL 标准 4 个隔离级别(由低到高):
- READ UNCOMMITTED 读未提交
- READ COMMITTED 读已提交 RC
- REPEATABLE READ 可重复读 RR(MySQL InnoDB 默认级别)
- SERIALIZABLE 串行化
前置基础概念
1)快照读 vs 当前读
- 快照读 (snapshot read):普通 select,读取 undo log 历史版本,不加行锁,依靠 MVCC。
select * from t where id=1;- 当前读 (current read):加锁读取最新版本,走行锁 / 临键锁,读取磁盘最新数据。
select ... for update; select ... lock in share mode; update / delete / insert2)MVCC 核心部件:undo log + read‑view + 隐藏列
每行记录 3 个隐藏列:
DB_TRX_ID:最后修改该行的事务 IDDB_ROLL_PTR:回滚指针,指向 undo log 里旧版本,形成版本链DB_ROW_ID:隐式主键(无主键时)
Read‑View(读视图):快照读的时候生成的一个视图对象,保存活跃事务 ID 集合,用来判断版本链上哪个版本对当前事务可见。
Read‑View 生成时机是 RC 和 RR 最核心区别!
1. READ UNCOMMITTED 读未提交
- 原理:不使用 MVCC,不生成 Read‑View
- 事务可以直接读到别的事务还没 commit 的数据(脏读)
- 读取直接拿最新行,不去 undo 版本链找历史快照
- 几乎生产不用。
问题:脏读 ✔,不可重复读 ✔,幻读 ✔
2. READ COMMITTED(RC)读已提交
✅实现原理
- 每次快照读(每执行一次普通 select,就重新生成一个新 Read‑View)
- 用 Read‑View 过滤 undo 版本链,只能读取已经提交事务所做的变更。
现象:
同一个事务内,前后两次相同select,中间别的事务提交修改;第二次 select 生成新 read‑view,可以读到别人提交后的新值 →出现不可重复读。
RC 级别:
- 脏读 ❌消除;
- 不可重复读 ✔存在;幻读 ✔存在
- RC 下 InnoDB 关闭 Gap 锁(没有间隙锁),只有记录锁;当前读只加行记录锁,不加临键锁,会出现幻读。
3. REPEATABLE‑READ(RR,InnoDB 默认)可重复读
✅实现原理
Read‑View 只在事务内 第一次快照读(第一条普通 select)的时候生成一次;之后整个事务复用这同一个 Read‑View,不再重新生成!
👉 所以同一个事务多次普通 select,始终拿这一套视图,读到同一套快照,消除了【不可重复读】(快照读层面)。
⚠重点:RR 隔离级别快照读 MVCC 不能解决幻读!
MVCC 只是读旧快照;别的事务依然可以真实插入新行,只是你快照看不到。
RR 消除幻读靠【当前读】时的临键锁 (Next‑Key Lock = 记录锁 + Gap 间隙锁)
- 当前读 (for update/update/delete) 时,会加 Next‑Key 临键锁锁住区间,阻止别的事务往这个区间插入新记录,从而防止幻读。
总结 RR:
- 脏读❌;不可重复读❌(快照读 MVCC)
- 幻读:快照读依然可以发生(只是看不见);当前读依靠临键锁防止幻读
RR 才开启 Gap 间隙锁;RC 没有间隙锁!这是 RC/RR 锁层面最大差异。
4. SERIALIZABLE 串行化
✅实现原理
不使用 MVCC 快照读了!
- InnoDB 把所有普通的快照读 select,隐式自动转成
SELECT ... LOCK IN SHARE MODE(加共享行锁) - 所有读操作都是当前读,读写互相阻塞;事务只能串行执行,完全隔离所有并发异常。
- 并发性能最差,生产极少使用。
- 脏读❌、不可重复读❌、幻读❌全部消除。
RC 与 RR 核心对比
| 项目 | RC (读已提交) | RR (可重复读 默认) |
|---|---|---|
| Read‑View 生成时机 | 每次 select 都新建 | 事务第一次 select 生成,复用到底 |
| MVCC 解决不可重复读 | ❌ | ✅快照读解决 |
| 间隙锁 Gap 锁 | 关闭,只有记录锁 | 开启 Next‑Key 临键锁 (记录 + 间隙) |
| 幻读 (当前读) | 存在幻读 | 临键锁阻止幻读 |
补充面试易错点
1.❌错误认知:RR 隔离级别 MVCC 直接解决幻读
✅正确:MVCC 只是快照;MVCC 本身不能阻止其他事务物理插入,只有临键锁(当前读)才阻止幻读。
2.RC 没有间隙锁,所以 update 范围查询的时候别的事务可以插入,发生幻读。
3.MVCC 只服务快照读;所有 DML (update/delete/for update) 永远都是当前读,不走快照。
一句话速记版
- RU:无 MVCC,直接读最新,脏读;
- RC:每次 select 生成 read‑view,消除脏读;无间隙锁,不可重复读、幻读存在;
- RR:事务首次 select 生成 read‑view 复用,MVCC 消除快照读不可重复读;开启临键锁,锁住区间防止当前读幻读;
- SERIALIZABLE:全部 select 隐式加共享锁,放弃快照,串行执行。
InnoDB 隔离级别:MVCC + 锁 分工
核心结论:
MVCC 负责「快照读 (普通 select)」;锁(行锁、Gap 间隙锁、Next‑Key 临键锁)负责「当前读 (for update /update/delete)」。两者各司其职,一起实现 4 种隔离级别。
1、先分清两类读(最重要)
① 快照读(Snapshot Read)
普通select * from t;
- ✅走MVCC,读 undo log 历史快照,不加锁
- MVCC 组件:隐藏列 DB_TRX_ID、DB_ROLL_PTR、undo log 版本链、Read‑View
- MVCC 只能解决读的可见性问题(脏读、不可重复读)
- MVCC无法物理阻止别的事务修改、插入数据;只是自己读到旧快照看不见新数据而已!
② 当前读(Current Read)
select ... for update select ... lock in share mode update / delete / insert- ✅走锁机制(行锁 / Gap 间隙锁 / Next‑Key 临键锁)
- 读取磁盘上最新的数据;靠锁物理阻塞其它事务的写、插入操作
- 用来解决幻读的是锁,不是 MVCC
2、四种隔离级别下 MVCC 与锁的开启情况
1. READ‑UNCOMMITTED 读未提交
- ❌不使用 MVCC、不生成 Read‑View;直接读取最新的数据行
- 锁:普通 select 不加锁;DML 依旧行锁;会出现脏读
2. READ‑COMMITTED RC 读已提交
- ✅使用 MVCC;每一次快照读都新建 Read‑View
- 🔒锁层面:关闭 Gap 间隙锁,只有记录锁(行锁)
- 现象:快照读会出现不可重复读;当前读范围查询会幻读(没有间隙锁挡插入)
3. REPEATABLE‑READ RR(MySQL 默认)
- ✅使用 MVCC;事务第一次快照读生成 Read‑View,整个事务复用这一份
MVCC:快照读层面消除不可重复读;但是仅仅是读旧快照!别的事务仍然可以物理插入新行。
- 🔒锁层面:开启 Next‑Key Lock(临键锁 = 记录锁 + Gap 间隙锁)
当前读的时候,临键锁锁住整个区间,物理阻止别的事务往区间插入新记录,从而解决当前读的幻读
⚠易错:快照读依旧 “伪幻读”:别的事务已经插进去了,只是 MVCC 快照看不到。
3. SERIALIZABLE 串行化
- ❌关闭 MVCC 快照读;普通 select 隐式变成
lock in share mode - 🔒全部变成当前读,加共享锁;读写互相阻塞,事务串行跑。
3、一句话区分分工
MVCC 解决快照读的可见性;锁解决当前读的并发写入阻塞。
RC/RR 的 Read‑View 时机控制 MVCC 可见性;RR 独有的临键锁负责物理阻挡插入,解决当前读幻读。
4、误区
1.❌错误:RR 隔离级别依靠 MVCC 解决幻读
✅正确:MVCC 只管读快照,拦不住别人插入;RR 是依靠临键锁 (Next‑Key Lock) 在当前读时防止幻读。
2.RC 级别没有间隙锁!只有 RR 才开启间隙锁。
3.MVCC 只作用于快照读;所有 DML 永远是当前读,不走 undo 快照版本链。
先把三个问题梳理
1)RC (读已提交) 为什么还要加行锁?
2)RR (可重复读) 为什么要加临键锁 Next‑Key Lock?
3)RR 到底有没有解决幻读?(重点易错)
1、RC 读已提交为什么加行锁?
⚠️记住:RC 只是关闭了 Gap 间隙锁,但是【记录锁(行锁)依然存在】
MVCC 仅仅作用于快照读(普通 select 不加锁)
update / delete / select … for update属于当前读,和 MVCC 无关!
RC 的锁策略:
- ✅快照读:MVCC,不加锁
- ✅当前读:只加记录锁(行锁),不加间隙锁
为什么 RC 还需要行锁?
行锁的作用是防止同一条记录被多个事务同时修改,解决写‑写冲突。
举例:事务 A update id=5;事务 B 也 update id=5;行锁拦住 B,避免两条事务同时改写同一行,丢失更新。
RC 只是不去锁间隙;也就是:同一行不能并发改,但是间隙之间允许别的事务插入新行。
👉所以 RC 范围的当前读就会出现幻读:锁住已经存在的行,但没锁住空隙,别人可以往空隙插入新记录。
2、RR 可重复读为什么引入临键锁 Next‑Key Lock(记录锁 + Gap 间隙锁)
临键锁 = 记录锁 + 间隙锁。
RR 的快照读靠 MVCC 解决不可重复读;但是 MVCC 拦不住别的事务 INSERT!
MVCC 只是读旧版本快照,做不到物理阻止其他事务在查询的区间插入新数据。
当执行范围的当前读,例如:
select * from t where id > 10 for update;如果只加普通行锁,只能锁住已经存在的那些行;id>10 的空隙没有锁,别的事务可以插入 id=11,12。 事务 A 再次做这条for update当前读,就会读到刚刚新插入的 id=11,发生幻读。
👉所以 InnoDB 在 RR 隔离级别,范围当前读时使用临键锁,把整个查询的区间全部锁住(包括中间空隙 Gap),禁止别的事务往区间里面 INSERT 新行。
目的:在当前读层面物理阻止幻读的发生。
补充小知识点:RR 也不是所有情况都一定生成临键锁;如果是等值查询,命中唯一索引精确匹配一行,会降级退化成普通记录锁。
3、RR 到底有没有解决幻读?(这道题坑最多)
幻读定义:同一个事务内,前后两次相同查询,第二次返回多出之前没有的行。
分两种读场景:
1. 快照读(普通 select,MVCC):RR 没有消除幻读(伪幻读)
事务 A 全程普通 select,复用同一份 read‑view;即使 B 事务已经物理插入提交了新行,A 读到的还是旧快照,看不见新增的数据。
⚠注意:数据库里面数据其实已经插进去了,只是 MVCC 快照看不到。
不是阻止了插入,只是看不见,这叫伪幻读。
2. 当前读(update /delete/for update):RR 依靠临键锁,物理锁住区间,禁止插入,解决了当前读场景下的幻读 。
✅最终标准答案
InnoDB 的 RR 隔离级别:
- 对于快照读(普通 select):依靠 MVCC,并没有真正解决幻读,只是看不到新插入的数据;
- 对于当前读(DML、for update 等):依靠临键锁 (Next‑Key Lock) 锁住查询区间,物理阻塞插入,解决幻读;所以不能笼统说 RR 完全解决幻读,也不能笼统说 RR 完全没解决幻读,要看是快照读还是当前读。
一句话总结
- RC 保留行 (记录) 锁是为了解写写冲突防止丢失更新;去掉间隙锁,允许间隙插入,并发更好。
- RR 增加临键锁(记录锁 + Gap),专门用来对付范围当前读时别的事务插入新行造成的幻读。
- RR 幻读分快照读和当前读两套结果,是 MVCC 和锁两套机制分别生效带来的现象。
拓展:SERIALIZABLE 隔离级别,全部读变成当前读加锁,所以不管快照读、当前读幻读全部消除。