MySQL 事务隔离级别与MVCC原理
一、事务ACID
事务 = 把一组SQL操作打包成一个”不可分割的整体”,要么全成功,要么全回滚。
| 特性 | 一句话 | 实现手段 |
|---|---|---|
| 原子性 | 全做或全不做 | undo log(回滚日志,存旧值) |
| 一致性 | 数据始终符合业务规则 | A + I + D 共同保证 |
| 隔离性 | 并发事务互不干扰 | MVCC + 锁 |
| 持久性 | 提交后就永久保存 | redo log(重做日志,崩溃恢复) |
-
undo log:改数据前把旧值记下来,回滚时按旧值恢复
-
redo log:提交时先写redo log(顺序追加极快),数据文件异步慢慢刷;崩溃后读redo log补刷
二、并发问题
2.1 脏读(Dirty Read)
-
读到了另一个事务还没提交的修改
-
那个事务如果回滚了,你读到的数据就是”脏的”(不存在的)
2.2 不可重复读(Non-Repeatable Read)
-
同一事务内,两次读同一条数据,结果不一样
-
原因:中间有另一个事务UPDATE并提交了
-
关注点:行的值变了
2.3 幻读(Phantom Read)
-
同一事务内,两次查同一条件,结果的行数不一样
-
原因:中间有另一个事务INSERT/DELETE并提交了
-
关注点:结果集的行数变了
| 问题 | 发生了什么 | 谁导致的 |
|---|---|---|
| 脏读 | 读到了未提交的修改 | 其他事务回滚 |
| 不可重复读 | 两次读同一条,值不同 | 其他事务UPDATE |
| 幻读 | 两次读同一条件,行数不同 | 其他事务INSERT/DELETE |
三、四种隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读(快照读) | 幻读(当前读) | 性能 |
|---|---|---|---|---|---|
| READ UNCOMMITTED | ❌有 | ❌有 | ❌有 | ❌有 | 最快 |
| READ COMMITTED | ✅防 | ❌有 | ❌有 | ❌有 | 快 |
| REPEATABLE READ | ✅防 | ✅防 | ✅防 | ⚠️部分防 | 中 |
| SERIALIZABLE | ✅防 | ✅防 | ✅防 | ✅防 | 最慢 |
-
MySQL默认:REPEATABLE READ
-
Oracle/PostgreSQL默认:READ COMMITTED
-
很多互联网公司实际用RC(并发性能更好,应用层可接受少量不一致)
RR没完全防住幻读的原因
-
普通SELECT(快照读):基于MVCC读事务开始时的版本,能防幻读
-
UPDATE/DELETE/SELECT FOR UPDATE(当前读):读最新提交版本,能看到新插入的行,可能产生幻读
四、MVCC原理
4.1 核心思想
-
每一行数据维护多个历史版本
-
事务根据自己的”出生时间”看对应版本的数据
-
读不加锁,读写不冲突,并发能力大幅提升
4.2 三大组件
隐藏列
每行数据(InnoDB表)都有三个隐藏列:
-
DB_TRX_ID:最后一次修改这一行的事务ID(6字节) -
DB_ROLL_PTR:指向undo log的指针,指向旧版本(7字节) -
DB_ROW_ID:隐藏主键(没指定主键时用,6字节)
Undo Log版本链
-
当前行通过DB_ROLL_PTR指向undo log中的旧版本
-
旧版本又通过自己的DB_ROLL_PTR指向更旧的版本
-
形成一条”历史版本链条”,从最新追溯到最初插入时
Read View(快照)
Read View包含四个字段:
-
m_ids:当前活跃(未提交)的事务ID列表 -
min_trx_id:活跃事务中最小的ID -
max_trx_id:下一个新事务的ID(系统当前最大ID + 1) -
creator_trx_id:创建Read View的事务自己的ID
4.3 可见性判断规则
拿到一个版本的DB_TRX_ID,按顺序判断:
1. 是自己改的? → 可见
2. trx_id < min_trx_id? → 可见(老早就提交了)
3. trx_id >= max_trx_id? → 不可见(还没出生,往前找旧版本)
4. 在min和max之间:
- 在m_ids里 → 不可见(还在晃悠没提交,往前找旧版本)
- 不在m_ids里 → 可见(已经提交了)
4.4 RC和RR的区别
| READ COMMITTED | REPEATABLE READ | |
|---|---|---|
| Read View创建时机 | 每次SELECT都重新创建 | 事务开始时创建一次,整个事务复用 |
| 结果 | 每次SELECT可能看到不同结果 | 整个事务期间看到的数据完全一样 |
| 防止不可重复读 | 不能(每次重建快照) | 能(一个快照用到底) |
五、快照读 vs 当前读
| 快照读 | 当前读 | |
|---|---|---|
| 触发SQL | 普通SELECT | UPDATE/DELETE/SELECT ... FOR UPDATE |
| 走不走MVCC | 走,读历史版本 | 不走,读最新提交版本 |
| 加不加锁 | 不加锁 | 加锁(行锁/间隙锁) |
| 能看到别人提交吗 | 看不到 | 能看到 |
六、间隙锁(Gap Lock)
-
RR隔离级别下,对当前读不光锁住已有行,还锁住索引之间的空隙
-
防止其他事务在该范围内INSERT(防幻读)
-
Next-Key Lock = 行锁 + 间隙锁(InnoDB在RR下的默认加锁方式)
-
副作用:降低并发——即使操作不同位置也可能互相阻塞
七、锁机制补充
行锁两种模式
| 锁类型 | 含义 | 触发方式 |
|---|---|---|
| 共享锁(S锁/读锁) | 可以读,别人也能读,但不能改 | SELECT ... FOR SHARE |
| 排他锁(X锁/写锁) | 只有你能读写,别人读写都不行 | UPDATE/DELETE/SELECT ... FOR UPDATE |
关键易错点
-
InnoDB行锁锁的是索引
-
如果WHERE条件没走索引,行锁退化成表锁
-
UPDATE user SET age = 30 WHERE name = '张三'— name没索引 → 全表加锁
八、一条UPDATE的全流程
1. 通过B+树定位到数据行所在的页 → 读入Buffer Pool
2. 加排他锁(X锁)
3. 记undo log(存旧值)
4. 修改Buffer Pool里的数据页
5. 记redo log(顺序追加)
6. COMMIT → 刷redo log到磁盘 → 持久性保证
7. 后台线程慢慢把Buffer Pool中改过的页刷到磁盘
九、binlog与redo log的区别
| redo log | binlog | |
|---|---|---|
| 层面 | InnoDB引擎层 | MySQL Server层 |
| 内容 | 物理日志(页的修改) | 逻辑日志(SQL语句或行变化) |
| 写入方式 | 循环写,空间固定 | 追加写,文件可切换 |
| 用途 | 崩溃恢复(保证持久性) | 主从复制、数据恢复 |
| 写入时机 | 事务执行中不断写 | 事务提交时一次性写 |
十、速答
| 问题 | 一句话答案 |
|---|---|
| ACID怎么实现? | A:undo log, C:A+I+D, I:MVCC+锁, D:redo log |
| 脏读/不可重复读/幻读区别? | 脏读=读未提交的值,不可重复读=同一行值变了(UPDATE),幻读=行数变了(INSERT) |
| MySQL默认隔离级别? | REPEATABLE READ |
| RC和RR根本区别? | RC每次SELECT建新Read View;RR一个事务建一次用到底 |
| MVCC是什么? | 每行多个历史版本,Read View决定能看到哪些版本 |
| RR能完全防幻读吗? | 快照读能(MVCC),当前读不能(需间隙锁) |
| InnoDB行锁锁的是什么? | 锁索引。没走索引时退化成表锁 |
十一、一图流(Mermaid)
flowchart TD A[并发事务] --> B{隔离?} B -->|不隔离| C[脏读/不可重复读/幻读] B -->|隔离 RR 默认| E[REPEATABLE READ 默认] E --> F{读的方式} F -->|快照读 SELECT| G[MVCC: 一个 Read View 用到底<br/>防脏读+不可重复读+快照幻读<br/>读不加锁, 读写不阻塞] F -->|当前读 UPDATE/DELETE| H[加锁: 行锁+间隙锁<br/>Next-Key Lock 防当前读幻读] G --> I[解决 读写冲突] H --> J[解决 写写冲突]
速记卡(面试闪卡)
Q1:一句话讲清「MySQL 事务隔离级别与MVCC原理」到底是什么?
A:事务隔离级别规定并发事务能看到什么数据,MVCC用多版本机制让读写互不阻塞。
Q2:一、事务ACID —— 怎么理解?
A:像发工资:要么整笔到账要么不发(原子性),账面始终对得上(一致性),俩人同时查互不串(隔离性),发了就改不了(持久性)。英文统称 ACID(Atomicity/Consistency/Isolation/Durability,原子性/一致性/隔离性/持久性)。
Q3:二、并发问题 —— 怎么理解?
A:像三个人同时改一张表:读到别人没提交的值(脏读 Dirty Read),同一条两次读出不一样(不可重复读 Non-Repeatable Read,值变了),同一条件两次查行数不同(幻读 Phantom Read,行多了)。三个坑就是并发不隔离的代价。
Q4:三、四种隔离级别 —— 怎么理解?
A:像四档窗帘:READ UNCOMMITTED 全透明(啥都能看到)最乱;READ COMMITTED 只信已提交;REPEATABLE READ(MySQL默认)整事务一个视角;SERIALIZABLE 全串行最稳但最慢。隔离档越高越安全越慢。
Q5:四、MVCC原理 —— 怎么理解?
A:像给每行数据拍连续快照:每行藏 DB_TRX_ID(谁改的)、回滚指针串成旧版本链,事务拿着自己的 Read View 按规则看”该看的那一版”。读不加锁、读写不打架。MVCC(Multi-Version Concurrency Control,多版本并发控制)。
Q6:核心速记主线有哪些?
-
隔离级别:RU / RC / RR / SERIALIZABLE,MySQL 默认 RR
-
并发三害:脏读、不可重复读、幻读(RR 快照读能防、当前读不能)
-
MVCC 三件套:隐藏列 + undo log 版本链 + Read View
-
行锁锁索引,没走索引退化成表锁
口诀
A:事务隔离管可见,MVCC 多版读写闲;
ACID 四性各有主,undo redo 锁相连;
RC 每次新快照,RR 一个用到底;
行锁锁在索引上,没索引就变表锁。
相关链接
-
📋 目录:00-MySQL
-
📚 学习清单:八股文学习路线图
-
🔗 线程同步锁机制 — MVCC是MySQL层面的并发控制,操作系统层面用锁机制
-
🔗 进程vs线程vs协程 — 并发的多个层次:OS级、数据库级、应用级
-
🔗 锁机制行锁表锁间隙锁 — MVCC解决读写冲突,锁机制解决写写冲突