MySQL(三):事务、隔离级别与 MVCC
MySQL(三):事务、隔离级别与 MVCC
导语:事务与 MVCC 是 MySQL 面试的分水岭。本篇覆盖 ACID 的实现依据、四种隔离级别与并发问题、MVCC 的三大组件(隐藏字段 / undo 版本链 / ReadView)、快照读与当前读,以及 RR 与 RC 的工程选型。共 16 题。
一、事务与 ACID
1. 什么是数据库事务?ACID 分别指什么?
答: 事务是逻辑上的一组操作,要么全部执行成功,要么全部不执行(如转账:扣款与入账必须同时成功或同时失败)。
ACID:
| 特性 | 含义 |
|---|---|
| Atomicity 原子性 | 事务内的操作要么全做,要么全不做,不存在"做一半" |
| Consistency 一致性 | 事务前后数据库都处于一致状态(完整性约束不被破坏,如转账前后总金额不变) |
| Isolation 隔离性 | 并发事务之间互不干扰,一个事务的中间状态对其他事务不可见 |
| Durability 持久性 | 事务提交后结果永久保存,即使宕机也不丢失 |
理解要点:一致性是目的,原子性、隔离性、持久性是手段。数据库机制能保证 A/I/D,但"业务一致性"(如转账金额不能为负)需要应用层约束配合——这是回答"一致性靠什么保证"时的关键区分(见下一题)。
2. ACID 在 MySQL/InnoDB 中分别靠什么保证?
答:
| 特性 | 依赖机制 | 原理 |
|---|---|---|
| 原子性 | Undo Log | 修改前先把旧值写入 undo log;事务失败/回滚时按 undo log 反向恢复 |
| 持久性 | Redo Log(WAL) | 提交时先把改动写入 redo log 并落盘,数据页可异步刷;崩溃后按 redo 重放 |
| 隔离性 | 锁 + MVCC | 写写冲突用锁互斥;读写冲突用 MVCC 实现"读不阻塞写、写不阻塞读" |
| 一致性 | 以上三者 + 应用层约束 | 数据库保证原子/持久/隔离,业务层面的正确性由应用保证 |
面试加分:不要把"一致性"也说成"靠数据库保证"。更准确的说法是:数据库通过 A/I/D 为一致性提供基础,但最终的业务一致性(约束校验、幂等、对账)必须由应用层负责。
3. MySQL 事务的实现原理(简述)?
答: InnoDB 通过日志 + 锁协同实现事务:
- 锁机制保障隔离性(写写互斥),行锁加在索引上;
- Redo Log 保障持久性(崩溃恢复时重放已提交的变更);
- Undo Log 保障原子性与一致性(回滚旧数据),同时支撑 MVCC 的版本链;
- 普通
SELECT走 MVCC 快照读,不加锁、不阻塞写;SELECT ... FOR UPDATE等当前读才加锁。
一个事务的完整链路:改数据前写 undo → 改 Buffer Pool 中的页 → 写 redo(prepare)→ 写 binlog → redo 置 commit(两阶段提交,见《MySQL(六)》)。
4. 并发事务会带来哪些问题?
答: 四类问题(前三个是标准定义的,第四个是工程中最常见的):
| 问题 | 现象 | 本质 |
|---|---|---|
| 脏读(Dirty Read) | 读到了别的事务未提交的数据;若对方回滚,读到的就是"从未存在过的数据" | 读了未提交版本 |
| 不可重复读(Non-Repeatable Read) | 同一事务内两次读同一行,值不一样 | 别的事务在两次读之间 UPDATE/DELETE 并提交 |
| 幻读(Phantom Read) | 同一事务内两次执行同一范围查询,结果集行数变了 | 别的事务在两次读之间 INSERT 并提交 |
| 丢失修改(Lost Update) | 两个事务都读到 A=20,各自做 A-1,最终 A=19 而非 18 | 先读后改的竞态,后写覆盖先写 |
不可重复读 vs 幻读的区分:不可重复读侧重"已有记录的值被改";幻读侧重"结果集的行数变化(新记录出现或消失)"。幻读可以看成不可重复读的一种特殊情况,但解决方案不同——不可重复读靠 MVCC 快照即可,幻读在当前读下必须靠间隙锁/Next-Key Lock 阻止插入。
5. 事务控制语句有哪些?丢失更新如何避免?
答: 控制语句:BEGIN / START TRANSACTION(开启)、COMMIT(提交)、ROLLBACK(回滚)、SAVEPOINT / ROLLBACK TO SAVEPOINT(部分回滚)、SET TRANSACTION(设置隔离级别)。
避免丢失更新(Lost Update)的三种方案:
| 方案 | 写法 | 特点 |
|---|---|---|
| 悲观锁 | SELECT ... FOR UPDATE 先锁住行再改 | 简单可靠,但持有锁期间会阻塞其他事务 |
| 乐观锁(版本号) | UPDATE t SET val=?, version=version+1 WHERE id=? AND version=? | 无锁、并发高;影响行数为 0 说明被改过,需业务重试 |
| 原子操作(推荐) | UPDATE account SET balance = balance - 1 WHERE id = ? | 把"读-改-写"压缩成一条 SQL,由数据库保证单语句原子性,无需显式加锁 |
最佳实践:能用原子操作就别用悲观锁。
SET balance = balance - 1这种写法天然避免了"先读后改"的窗口,是并发扣减的首选;只有在需要"读出来做复杂判断"时才用FOR UPDATE。
二、隔离级别
6. SQL 标准定义了哪四种隔离级别?各自解决什么问题?
答: 隔离级别越高,一致性越强、并发性能越差:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ-UNCOMMITTED(读未提交) | 可能发生 | 可能发生 | 可能发生 |
| READ-COMMITTED(读已提交) | 不会发生 | 可能发生 | 可能发生 |
| REPEATABLE-READ(可重复读) | 不会发生 | 不会发生 | 可能发生(InnoDB 实际可防) |
| SERIALIZABLE(串行化) | 不会发生 | 不会发生 | 不会发生 |
逐级递进关系:
- RU:允许读到未提交数据,几乎不隔离,仅用于对一致性无要求的统计场景;
- RC:只挡住脏读(Oracle、PostgreSQL 的默认级别);
- RR:再挡住不可重复读,是 MySQL InnoDB 的默认级别;
- SERIALIZABLE:读也加锁、完全串行,一致性最强但并发最差,生产几乎不用。
InnoDB 的增强:SQL 标准的 RR 允许幻读,但 InnoDB 通过 MVCC(快照读)+ Next-Key Lock(当前读) 在 RR 下实际解决了幻读。所以上面表格里 RR 的幻读标的是"标准允许、InnoDB 实际可防"——回答时一定要把这一点讲出来。
7. MySQL 默认隔离级别是什么?如何查看和设置?
答: InnoDB 默认是 REPEATABLE-READ(可重复读)。
-- MySQL 8.0:查看当前会话隔离级别
SELECT @@transaction_isolation;
-- 查看全局隔离级别
SELECT @@global.transaction_isolation;
-- 设置(会话级)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置(全局级,影响后续新连接)
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;版本差异:
- MySQL 5.7 及之前:变量名为
@@tx_isolation; - MySQL 8.0 起:改为
@@transaction_isolation(tx_isolation已废弃)。
配置文件写法:
transaction-isolation = READ-COMMITTED;也可在my.cnf的[mysqld]段设置。
8. 不可重复读和幻读的区别?
答: 两者都表现为"同一事务内多次查询结果不一致",区别在变化的性质:
| 维度 | 不可重复读 | 幻读 |
|---|---|---|
| 变化的对象 | 同一条已有记录的值 | 结果集的行数 |
| 由什么引起 | 别的事务 UPDATE/DELETE 并提交 | 别的事务 INSERT 并提交 |
| 典型现象 | 两次读 id=1,余额从 100 变成 200 | 两次 COUNT(*) WHERE age>18,从 10 变成 11 |
| 解决手段 | MVCC 快照读即可(RR 下一份 ReadView 复用到底) | 快照读靠 MVCC;当前读必须靠 Next-Key Lock 锁住范围防插入 |
一句话:不可重复读是"值变了",幻读是"行多了/少了"。
9. MySQL 的隔离级别是基于锁还是 MVCC 实现的?
答: 两者结合,按隔离级别不同而侧重不同(原文此处对 RU 的描述不准确,需要纠正):
| 隔离级别 | 实现方式 |
|---|---|
| READ-UNCOMMITTED | 不使用 MVCC。直接读取最新版本(包括其他事务尚未提交的修改),因此才有脏读 |
| READ-COMMITTED | 使用 MVCC,但每次 SELECT 都重新生成 ReadView |
| REPEATABLE-READ | 使用 MVCC,事务内第一次 SELECT 生成 ReadView 后复用;当前读额外加 Next-Key Lock |
| SERIALIZABLE | 主要靠加锁(读加共享锁、写加排他锁),读写串行 |
易错点纠正:不能说"RU/RC/RR 都是基于 MVCC"。RU 下 InnoDB 并不生成 ReadView,它读的就是最新版本——这正是"读未提交"的字面含义。
三、MVCC 原理
10. 什么是 MVCC?它依赖什么实现?
答: MVCC(Multi-Version Concurrency Control,多版本并发控制) 通过对同一份数据保存多个历史版本,让不同事务看到各自应该看到的"快照",从而实现:
- 读不阻塞写、写不阻塞读(避免读写互相加锁);
- 读读之间天然不冲突。
InnoDB 的 MVCC 依赖三大组件:
| 组件 | 作用 |
|---|---|
| 1. 隐藏字段 | 每行记录都有——DB_TRX_ID(最近修改该行的事务 ID)、DB_ROLL_PTR(回滚指针,指向 undo log 中的上一个版本)、DB_ROW_ID(无主键时用的隐式行 ID) |
| 2. Undo Log | 保存数据的历史版本,通过 DB_ROLL_PTR 把各版本串成版本链(新版本 → 旧版本) |
| 3. ReadView(一致性视图) | 快照读时生成,用于判断版本链上哪个版本对当前事务可见 |
记忆链条:隐藏字段标记"谁改的、上一版在哪" → undo log 提供"上一版的内容" → ReadView 决定"我能看哪一版"。
11. MVCC 如何解决不可重复读?RR 下如何防幻读?
答:
1)如何解决不可重复读
RR 级别下,事务在第一次快照读时生成 ReadView,并在整个事务内复用。之后无论其他事务如何修改、提交,本事务沿版本链查找时都会用同一个 ReadView 判断可见性,因此多次读到的数据完全一致。
2)RR 下如何防幻读(分两种读法)
| 读法 | 防幻读机制 |
|---|---|
快照读(普通 SELECT) | 靠 MVCC:ReadView 固定,新插入的记录其 DB_TRX_ID 晚于本事务,判定为不可见,所以看不到"凭空多出来的行" |
当前读(SELECT ... FOR UPDATE / LOCK IN SHARE MODE / UPDATE / DELETE) | 靠 Next-Key Lock(记录锁 + 间隙锁):把查询范围内的间隙也锁住,别的事务无法在范围内插入新记录 |
为什么当前读必须有间隙锁:当前读要读"最新已提交数据",MVCC 帮不上忙;若只锁已有记录,别的事务仍能在间隙里插入新行,下次当前读就会多出记录(幻读)。因此必须锁住间隙。
结论句式:"InnoDB 的 RR 通过 MVCC 解决快照读的幻读,通过 Next-Key Lock 解决当前读的幻读,因此实际达到了防幻读的效果——这是它对 SQL 标准 RR 的增强。"
12. 快照读与当前读的区别?
答:
| 维度 | 快照读(一致性读) | 当前读(锁定读) |
|---|---|---|
| 触发语句 | 普通 SELECT | SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、INSERT、UPDATE、DELETE |
| 读到什么 | ReadView 生成那一刻的一致性快照(可能是历史版本) | 最新已提交版本 |
| 是否加锁 | 不加锁 | 加锁(S 锁或 X 锁) |
| 是否阻塞 | 不阻塞任何写操作 | 会阻塞其他事务的读写(视锁类型) |
| 典型用途 | 日常查询、报表统计 | 需要"读到最新值并以此做后续修改"的场景(如扣库存、状态机流转) |
实践意义:正因为快照读不加锁,InnoDB 才能做到"高并发下读性能不受写影响"。而
UPDATE默认就是当前读——它会先读最新版本(并加 X 锁)再修改,所以不要以为UPDATE会基于快照。
13. 什么是 ReadView?RC 与 RR 下 ReadView 生成时机有何不同?
答: ReadView 是快照读时生成的一致性视图,包含四个关键字段:
| 字段 | 含义 |
|---|---|
m_ids | 生成 ReadView 时所有活跃(未提交)事务的 ID 集合 |
min_trx_id | m_ids 中的最小值 |
max_trx_id | 下一个将要分配的事务 ID(即已分配的最大 ID + 1) |
creator_trx_id | 创建该 ReadView 的事务自己的 ID |
可见性判断规则(对某行版本的 DB_TRX_ID 逐条判断):
DB_TRX_ID == creator_trx_id→ 可见(是我自己改的);DB_TRX_ID < min_trx_id→ 可见(该事务在我之前就已提交);DB_TRX_ID >= max_trx_id→ 不可见(该事务在我创建视图之后才开始);min_trx_id <= DB_TRX_ID < max_trx_id→ 看它是否在m_ids中:在 → 不可见(说明当时还活跃);不在 → 可见(说明已提交)。
若当前版本不可见,就沿 DB_ROLL_PTR 沿版本链向前找,直到找到第一个可见版本。
RC 与 RR 的关键差异:
| 级别 | ReadView 生成时机 | 后果 |
|---|---|---|
| READ-COMMITTED | 每次快照读都重新生成 | 能读到别的事务刚提交的修改 → 出现不可重复读 |
| REPEATABLE-READ | 事务内第一次快照读生成,之后复用 | 后续读都基于同一视图 → 可重复读 |
一句话总结:RC 与 RR 在 MVCC 上的唯一区别就是 ReadView 的生成时机——这也是"为什么同一个 MVCC 机制能支撑两种隔离级别"的答案。
14. Undo Log 在 MVCC 中的作用与版本链?
答: 每次修改记录前,InnoDB 会把修改前的旧值写入 undo log,并让当前记录的 DB_ROLL_PTR 指向这个 undo 记录;undo 记录之间又通过回滚指针继续向前指向更早的版本,最终形成一条版本链:
当前记录 → 版本 v3 → 版本 v2 → 版本 v1 → 更早
(DB_TRX_ID 最大) (DB_TRX_ID 最小)MVCC 如何使用它:快照读时,从最新版本开始,逐版本用 ReadView 判断可见性,第一个可见的版本就是本次查询应该返回的数据。
Undo Log 的双重身份:
- 回滚(原子性):事务失败或
ROLLBACK时,按版本链把数据恢复到事务开始前; - MVCC 快照读:提供历史版本,是"多版本"的数据来源。
Undo 的分类与清理:
insert undo:插入操作的 undo,事务提交后即可删除(没有其他事务需要看它之前的状态);update undo:更新/删除的 undo,不能立即删除——因为可能还有更早的 ReadView 需要读它;由后台 purge 线程在"确认没有任何活跃 ReadView 需要该版本"后清理;- 因此长事务会阻止 purge,导致 undo 空间持续膨胀(这是"禁止长事务"的核心原因,详见《MySQL(六)》)。
15. SERIALIZABLE 级别如何避免幻读?
答: SERIALIZABLE 是最高隔离级别,通过"读也加锁"实现完全的串行化:
- 普通
SELECT会自动转成SELECT ... LOCK IN SHARE MODE(加共享锁 S); UPDATE/DELETE加排他锁 X;- 需要防幻读的范围查询会锁住整个范围(含间隙),其他事务无法插入新记录。
代价:读写互相阻塞、大量锁等待,并发度最低,因此生产环境基本不用。
实践中的替代方案(更推荐):
| 目标 | 更实用的做法 |
|---|---|
| 防止重复插入 | 唯一索引 + INSERT ... ON DUPLICATE KEY UPDATE(比锁范围更轻量、更精确) |
| 防超卖/超卖扣减 | UPDATE ... SET stock = stock - 1 WHERE id = ? AND stock > 0(原子操作) |
| 悲观串行控制 | SELECT ... FOR UPDATE 精确锁住需要的那几行 |
面试话术:被问到"怎么防幻读"时,不要一上来就说 SERIALIZABLE。标准答案的顺序应该是:① RR 下的 MVCC + Next-Key Lock(InnoDB 默认就能防);② 唯一索引从业务上根治;③ SERIALIZABLE 作为最后手段(性能代价大)。
16. RR 和 RC 该怎么选?为什么很多公司改用 RC?
答: 这是一个很有价值的工程题,答案在"一致性"与"并发/可运维性"之间权衡。
RR(MySQL 默认)的特点:
- 事务内多次读完全一致,业务逻辑更"可预期";
- 但间隙锁会带来更多锁冲突与死锁,且长事务会拖住 undo purge。
RC 的特点与优势(互联网公司常改成 RC):
| 优势 | 说明 |
|---|---|
| 锁冲突更少 | RC 禁用间隙锁,只加记录锁,大幅降低锁范围、减少死锁 |
| 无"当前读+快照读"混合的诡异现象 | RR 下同一事务里"快照读看到旧值、当前读看到新值",容易让业务代码踩坑;RC 语义更直白 |
| 兼容其他数据库 | Oracle、PostgreSQL 默认都是 RC,跨库迁移与团队习惯更一致 |
| undo 压力更小 | 每次读生成新 ReadView,长事务对版本链的要求低于 RR |
| 主从一致性更好保证 | 与 binlog ROW 格式配合时,RC 的复制语义更可靠 |
如何选择:
- 金融、账务等强一致性场景 → 倾向 RR(或更谨慎的显式加锁设计);
- 互联网高并发业务 → 常用 RC,用业务层的乐观锁/唯一索引替代"依赖间隙锁防幻读";
- 无论选哪个:都不该依赖隔离级别去解决业务一致性问题——唯一索引、原子操作、幂等设计才是更可靠的防线。
设置方式:
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;,或在my.cnf中写transaction-isolation = READ-COMMITTED。
