📖 本篇简介:隔离级别是事务篇的"第二战场",也是面试官最爱深挖的考点。本篇用 17 道高频题讲清四个隔离级别分别解决什么并发问题、脏读/不可重复读/幻读的区别、InnoDB 在 RR 级别下怎么用 Next-Key Lock 挡住幻读、当前读与快照读的差异,以及为什么互联网公司普遍偏爱 RC。学完本篇,你能在白板上画出两个会话的执行时序,并准确说出每一步看到的结果。前置知识:第 3 章事务与 ACID;本系列示例基于 MySQL 8.0。
目录
- 1 隔离级别高频考点地图
- 题目 1SQL 标准的四个隔离级别分别解决什么问题
- 题目 2MySQL 默认隔离级别是什么和 Oracle 有何不同
- 题目 3什么是脏读哪个级别能避免
- 题目 4什么是不可重复读哪个级别能避免
- 题目 5什么是幻读InnoDB RR 怎么避免幻读
- 题目 6RC 和 RR 的快照生成时机有什么不同
- 题目 7当前读和快照读有什么区别
- 题目 8SELECT FOR UPDATE 与 LOCK IN SHARE MODE 的区别
- 题目 9RR 下普通 SELECT 看不到新行但当前读能看到这是幻读吗
- 题目 10怎么设置隔离级别全局与会话级
- 题目 11隔离级别越高越好吗
- 题目 12什么是 GAP 锁什么情况下会加
- 题目 13NextKey Lock 是什么
- 题目 14为什么互联网公司常用 RC 而不是 RR
- 题目 15RR 级别下真的完全杜绝幻读了吗
- 题目 16怎么查看当前会话的隔离级别与锁状态
- 题目 17串行化 SERIALIZABLE 怎么实现什么场景用
- 隔离级别速查表
- 常见坑与注意事项
- 复习清单
- 练习题
1 隔离级别高频考点地图
面试里"隔离级别"类问题的常见问法:
| 难度 | 典型问题 | 本篇对应 |
|---|---|---|
| 入门 | 四个隔离级别?解决什么问题? | 题目 1、2 |
| 基础 | 脏读/不可重复读/幻读区别? | 题目 3、4、5 |
| 进阶 | 当前读/快照读?Next-Key Lock? | 题目 7、8、13 |
| 实战 | 为什么用 RC?幻读真没了吗? | 题目 14、15 |
一条主线记住:隔离级别 = 多严格的快照 + 多宽的锁范围。级别越低性能越高、脏读风险越大;级别越高锁越宽、越容易死锁。
题目 1:SQL 标准的四个隔离级别分别解决什么问题?
考点
- READ UNCOMMITTED / READ COMMITTED / REPEATABLE READ / SERIALIZABLE;
- 三个并发异常:脏读、不可重复读、幻读;
- 级别与异常的对应关系。
参考答案
SQL 标准定义四个隔离级别,从低到高:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED(读未提交) | ❌ 会出现 | ❌ 会出现 | ❌ 会出现 |
| READ COMMITTED(读已提交 RC) | ✅ 避免 | ❌ 会出现 | ❌ 会出现 |
| REPEATABLE READ(可重复读 RR) | ✅ 避免 | ✅ 避免 | ⚠️ 标准允许出现,InnoDB 避免 |
| SERIALIZABLE(串行化) | ✅ 避免 | ✅ 避免 | ✅ 避免 |
- 脏读:读到别的事务未提交的修改;
- 不可重复读:同一事务内两次读同一行,结果不一样(被别的事务 UPDATE/COMMIT);
- 幻读:同一事务内两次范围查询,行数不一样(被别的事务 INSERT/DELETE/COMMIT)。
-- 查看当前隔离级别(MySQL 8.0)
SELECT @@transaction_isolation; -- 8.0 用 transaction_isolation,5.7 用 tx_isolation
-- 默认输出 REPEATABLE-READ
追问
- 问:InnoDB 的 RR 比 SQL 标准的 RR 还严格?答:是,InnoDB 在 RR 下通过 Next-Key Lock 解决了幻读,这是它比其他数据库(如 PostgreSQL)更"强"的地方。
- 问:RU 什么时候用?答:几乎不用,监控统计、对准确性要求极低的场景才考虑。
题目 2:MySQL 默认隔离级别是什么?和 Oracle 有何不同?
考点
- InnoDB 默认 RR;
- Oracle/PostgreSQL 默认 RC;
- 历史原因与兼容性。
参考答案
- MySQL InnoDB 默认是 RR(REPEATABLE-READ);
- Oracle、PostgreSQL 默认是 RC(READ COMMITTED);
- 历史原因:MySQL 早期要做主从复制的"语句级安全",RR 在 STATEMENT 格式 binlog 下能避免主从数据不一致;8.0 已经默认 ROW 格式,这个理由弱化,但默认值仍保持 RR 以兼容老业务;
- 互联网大厂生产配置大多改成 RC,原因详见题目 14。
-- 8.0 my.cnf 设置默认隔离级别为 RC
-- [mysqld]
-- transaction_isolation = READ-COMMITTED
-- 动态修改全局:
SET GLOBAL transaction_isolation = 'READ-COMMITTED';
追问
- 问:改默认隔离级别要重启吗?答:全局参数可以动态 SET GLOBAL,但已建立的连接仍用旧级别,新连接才生效;要全量生效需滚动重启或让连接池重建连接。
- 问:从 RR 改成 RC 要注意什么?答:RC 没有 Next-Key Lock 的间隙锁,并发插入更自由,但业务若依赖 RR 的"可重复读"语义要重新评估。
题目 3:什么是脏读?哪个级别能避免?
考点
- 读到未提交数据;
- RU 才会出现;
- RC 及以上避免。
参考答案
- 脏读(Dirty Read):一个事务读到了另一个事务还没提交的修改。如果对方回滚,自己读到的就是"根本不存在过"的数据;
- 只有 RU 级别会出现脏读;RC/RR/SERIALIZABLE 都通过 MVCC 读快照或读已提交数据避免了脏读;
- 实战中脏读几乎不会发生(没人用 RU),但面试必问概念。
-- 脏读演示(必须先 SET SESSION tx_isolation='READ-UNCOMMITTED')
-- 会话 A:
START TRANSACTION;
UPDATE account SET balance = 1 WHERE id = 1; -- 未提交
-- 会话 B(RU 级别):
SELECT balance FROM account WHERE id = 1; -- 看到 1(脏读)
-- 会话 A:ROLLBACK
-- 会话 B 再读:又变回原来的值
追问
- 问:RC 下会脏读吗?答:不会,RC 只能读到别的事务已经提交的数据。
- 问:脏读和不可重复读的区别?答:脏读是读了未提交的;不可重复读是两次读都读到了已提交的,但两次结果不一样。
题目 4:什么是不可重复读?哪个级别能避免?
考点
- 同一事务两次读同一行结果不同;
- 被别的事务 UPDATE 提交;
- RR 级别通过快照避免。
参考答案
- 不可重复读(Non-repeatable Read):同一个事务内,两次执行同一条 SELECT,读到的同一行数据前后不一样——因为中间被别的事务 UPDATE 并 COMMIT 了;
- 关注点在同一行内容变化(UPDATE);
- RC 级别会出现(每次 SELECT 都生成新快照,看到最新提交);RR 级别避免(事务启动后第一次 SELECT 生成 ReadView,之后一直用这个快照)。
-- RC 下不可重复读演示
-- 会话 A(RC):
START TRANSACTION;
SELECT balance FROM account WHERE id = 1; -- 100
-- 会话 B:UPDATE account SET balance = 50 WHERE id = 1; COMMIT;
-- 会话 A:
SELECT balance FROM account WHERE id = 1; -- 50(不可重复读)
追问
- 问:不可重复读和幻读的区别?答:不可重复读针对同一行被 UPDATE;幻读针对新行被 INSERT/DELETE 导致行数变了。
- 问:业务上能容忍不可重复读吗?答:多数"对账、报表"场景不能容忍,因为要保证事务内视图一致;普通请求级读大多无所谓。
题目 5:什么是幻读?InnoDB RR 怎么避免幻读?
考点
- 同一事务两次范围查询行数变化;
- InnoDB RR 用 Next-Key Lock;
- 快照读 + 当前读的差异。
参考答案
- 幻读(Phantom Read):同一事务内两次相同的范围查询,返回的行数不一样——中间被别的事务 INSERT/DELETE 并 COMMIT;
- SQL 标准下 RR 允许幻读;但 InnoDB 在 RR 级别用 Next-Key Lock(记录锁 + 间隙锁)锁住查询涉及的范围,阻止其他事务在间隙中插入,从而避免幻读;
- 注意:这个"避免"主要对当前读生效;普通快照读靠 ReadView 本来就看不到新行。
-- RR 下 Next-Key Lock 挡住幻读
-- 会话 A:
START TRANSACTION;
SELECT * FROM t_user WHERE age BETWEEN 20 AND 30 FOR UPDATE; -- 当前读,加 Next-Key Lock
-- 会话 B:
INSERT INTO t_user(age) VALUES(25); -- ❌ 阻塞,25 在间隙锁覆盖范围内
-- 会话 A:再次同样 SELECT,行数不变;COMMIT 后会话 B 才能插入
追问
- 问:快照读能完全避免幻读吗?答:能,因为 ReadView 固定,看不到别的事务提交的新行;但这只是"看起来没有幻读",不是靠锁。
- 问:幻读会导致什么 bug?答:典型"先查再插"的并发:事务 A 查不到某手机号就插入,事务 B 也查不到也插入,最终唯一索引冲突报错——这就是为什么"先 SELECT 再 INSERT"一定要加唯一索引兜底。
题目 6:RC 和 RR 的快照生成时机有什么不同?
考点
- ReadView 生成时机;
- RC 每条语句生成新 ReadView;
- RR 事务启动后第一次 SELECT 生成 ReadView。
参考答案
- MVCC 靠 ReadView(一致性视图) 判断哪个版本可见;
- RC:每执行一条 SELECT 都生成新的 ReadView,所以能看到别的事务最新提交;
- RR:事务启动后第一次执行 SELECT 时生成 ReadView,整个事务复用这个 ReadView,之后都看不到别的事务新提交;
- 这就是 RC"不可重复读"、RR"可重复读"的根本原因。
-- RC vs RR 的时序对比
-- RR 下:
START TRANSACTION;
SELECT ...; -- 此刻生成 ReadView,后续都用它
SELECT ...; -- 结果相同(可重复读)
COMMIT;
-- RC 下:
START TRANSACTION;
SELECT ...; -- 新 ReadView
SELECT ...; -- 又一个新 ReadView,可能看到新提交(不可重复读)
追问
- 问:RR 下 START TRANSACTION 立刻生成 ReadView 吗?答:不是,是第一次普通 SELECT 才生成;
START TRANSACTION WITH CONSISTENT SNAPSHOT才立即生成。 - 问:READ ONLY 事务有优化吗?答:8.0 对只读事务的 ReadView 创建做了优化,开销更小。
题目 7:当前读和快照读有什么区别?
考点
- 普通 SELECT = 快照读;
- FOR UPDATE / FOR SHARE / UPDATE / DELETE = 当前读;
- 当前读读最新版本并加锁。
参考答案
- 快照读(Consistent Read):普通
SELECT,不加锁,基于 ReadView 读历史版本; - 当前读(Current Read):读最新已提交版本,并对记录加锁。包括:
SELECT ... FOR UPDATE(X 锁);SELECT ... FOR SHARE / LOCK IN SHARE MODE(S 锁);UPDATE / DELETE(自带 X 锁);- 当前读必须看到最新数据(否则 UPDATE 会覆盖丢更新),所以走最新版本 + 加锁。
-- 同一事务内两种读对比(RR 级别)
START TRANSACTION;
SELECT balance FROM account WHERE id=1; -- 快照读,100
-- 别的事务把 balance 改成 50 并提交
SELECT balance FROM account WHERE id=1; -- 快照读,仍是 100
SELECT balance FROM account WHERE id=1 FOR UPDATE;-- 当前读,50(最新已提交)
COMMIT;
追问
- 问:为什么 UPDATE 是当前读?答:UPDATE 必须基于最新值做修改,否则会丢失别的事务刚提交的更新(Lost Update)。
- 问:快照读能看到自己事务所做的修改吗?答:能,ReadView 自己永远可见,这就是"写后读一致"。
题目 8:SELECT FOR UPDATE 与 LOCK IN SHARE MODE 的区别?
考点
- X 锁 vs S 锁;
- FOR SHARE 是 8.0 新写法;
- 锁的兼容性。
参考答案
SELECT ... FOR UPDATE:加 X 锁(排他锁),其他事务不能再加任何锁,也不能 UPDATE;SELECT ... LOCK IN SHARE MODE(8.0 推荐新写法FOR SHARE):加 S 锁(共享锁),其他事务可以再加 S 锁读,但不能写;- 典型场景:下单时
SELECT ... FOR UPDATE锁住库存行再扣减,防止超卖。
-- 扣库存标准写法(RR 级别)
START TRANSACTION;
SELECT stock FROM sku WHERE sku_id = 100 FOR UPDATE; -- 加 X 锁
UPDATE sku SET stock = stock - 1 WHERE sku_id = 100;
-- 校验 stock>0 后 COMMIT
COMMIT;
追问
- 问:FOR SHARE 和普通 SELECT 有什么区别?答:FOR SHARE 加了 S 锁,阻塞别人的写;普通 SELECT 快照读不加锁。
- 问:锁在什么时候释放?答:事务 COMMIT/ROLLBACK 时释放,不是语句结束就放——这就是长事务拖死业务的原因。
题目 9:RR 下普通 SELECT 看不到新行但当前读能看到,这是幻读吗?
考点
- ReadView 与最新版本的差异;
- 业务上的"不一致读";
- 典型 bug 场景。
参考答案
- 这是 InnoDB RR 下一个经典"幻读感":
- 会话 A 普通 SELECT 用 ReadView,看不到别的事务新提交的行;
- 同一会话 A 随后
SELECT ... FOR UPDATE(当前读),看到了这些新行; - 从 MVCC 角度不算 bug(快照读本来就该读老版本),但业务上会觉得"我刚刚查过没有,怎么现在冒出来了";
- 解决:事务内若要"读+判断+写",全程用当前读(FOR UPDATE),别混用快照读和当前读。
-- 典型坑:先快照读判断不存在,再插入
START TRANSACTION;
SELECT * FROM user WHERE phone = '138xxxx'; -- 快照读,无
-- 别的事务插入同 phone 并提交
INSERT INTO user(phone) VALUES('138xxxx'); -- ❌ 唯一索引冲突报错
-- 正确:用当前读判断
SELECT * FROM user WHERE phone='138xxxx' FOR UPDATE; -- 看到新行,走更新分支
追问
- 问:那 RR 到底有没有幻读?答:当前读 + Next-Key Lock 挡住了幻读;但快照读和当前读混用会出现"逻辑上的幻读感",面试要分两层答。
- 问:唯一索引是不是必须的?答:是,并发"先查再插"场景唯一索引是最后兜底,别只靠业务判断。
题目 10:怎么设置隔离级别?全局与会话级?
考点
- SET GLOBAL / SESSION;
- my.cnf 持久化;
- 动态参数生效范围。
参考答案
-- 查看(MySQL 8.0)
SELECT @@global.transaction_isolation;
SELECT @@session.transaction_isolation;
-- 会话级(只影响当前连接)
SET SESSION transaction_isolation = 'READ-COMMITTED';
-- 全局级(影响之后新建的连接,已有连接不变)
SET GLOBAL transaction_isolation = 'READ-COMMITTED';
-- 也可以在事务级别单独指定(8.0)
START TRANSACTION READ ONLY;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
- my.cnf 持久化:
[mysqld] transaction_isolation = READ-COMMITTED; - 注意:SET GLOBAL 不影响现有连接,连接池里的老连接还是老级别,要让所有连接生效必须重建连接或重启。
追问
- 问:5.7 和 8.0 参数名区别?答:5.7 是
tx_isolation,8.0 改成transaction_isolation,老参数 8.0 仍兼容。 - 问:一个事务内能改隔离级别吗?答:事务内改不生效,要在事务开始前设置。
题目 11:隔离级别越高越好吗?
考点
- 高隔离 = 锁更宽 + 并发更低;
- 业务按需选择;
- 没有"银弹"。
参考答案
- 不是。隔离级别越高,锁范围越大、并发越低、越容易死锁和锁等待:
- SERIALIZABLE 把所有 SELECT 加读锁,吞吐暴跌;
- RR 比 RC 多了间隙锁,并发插入更容易互相阻塞;
- 选择原则:
1. 互联网业务:RC 足够,并发性能好;
2. 金融对账、强一致需求:RR 甚至分布式事务;
3. SERIALIZABLE:几乎不用,性能差。
- 一句话:按业务对"正确性 vs 吞吐"的权衡选,别盲目追高。
追问
- 问:RC 下有间隙锁吗?答:RC 没有 Next-Key Lock 的间隙锁(除了外键和唯一性检查),所以并发插入更自由、死锁更少。
- 问:RR 改 RC 后主从复制要注意什么?答:binlog 必须用 ROW 格式,否则 STATEMENT 下 RC 可能主从不一致。
题目 12:什么是 GAP 锁?什么情况下会加?
考点
- GAP = 索引记录之间的间隙;
- 防止幻读;
- RC 下基本不加。
参考答案
- GAP 锁(间隙锁):锁的是索引记录之间的开区间,不锁记录本身,只阻止别人在这个间隙里 INSERT;
- 举例:索引上有 10、20 两条记录,间隙
(10, 20)被加 GAP 锁后,其他事务不能插入 15; - RR 级别下,范围查询的当前读会加 Next-Key Lock(记录锁 + GAP 锁);
- RC 级别不加间隙锁(外键和唯一索引检查例外),这是 RC 并发更高的重要原因。
-- RR 下间隙锁阻塞插入
-- 表 t:id 有 10, 20
-- 会话 A:
START TRANSACTION;
SELECT * FROM t WHERE id BETWEEN 10 AND 20 FOR UPDATE; -- 锁 (10,20] 这段 Next-Key
-- 会话 B:
INSERT INTO t VALUES(15); -- ❌ 阻塞,15 在间隙里
追问
- 问:GAP 锁之间兼容吗?答:GAP 锁不阻止别人再加 GAP 锁,只阻止插入,所以两个事务可以同时锁同一个间隙。
- 问:唯一索引的等值查询会加 GAP 锁吗?答:唯一索引等值查到存在的记录,退化成记录锁;查不到时会加 GAP 锁防幻插。
题目 13:Next-Key Lock 是什么?
考点
- 记录锁 + 间隙锁;
- 左开右闭区间;
- InnoDB RR 的幻读防线。
参考答案
- Next-Key Lock = 记录锁(Record Lock)+ 间隙锁(GAP Lock);
- 结构是左开右闭区间:对索引上的记录来说,Next-Key Lock 锁的是
(前一条记录, 当前记录]; - InnoDB 在 RR 级别下,范围查询的当前读默认加 Next-Key Lock,把整个范围连同间隙一起锁住,其他事务既不能改已有行、也不能在间隙里插入——这就是 RR 防幻读的核心机制;
- 优化:如果查询精确命中唯一索引,Next-Key Lock 会退化成记录锁;若等值查不到,则退化成 GAP 锁。
-- 索引记录:10, 20, 30
-- SELECT * FROM t WHERE id > 5 AND id < 25 FOR UPDATE;
-- 加的 Next-Key Lock 区间:
-- (5, 10]、(10, 20]、(20, 25] —— 整个左开右闭范围全锁
-- 其他事务插入 15、22 都会阻塞
追问
- 问:Next-Key Lock 会不会导致锁范围过大?答:会,这就是 RR 下并发插入容易死锁的原因;RC 去掉间隙锁就是为了这个。
- 问:怎么看当前有哪些 Next-Key Lock?答:
performance_schema.data_locks表(8.0),LOCK_MODE列显示X,GAP或X,Next-Key。
题目 14:为什么互联网公司常用 RC 而不是 RR?
考点
- RC 无间隙锁,并发高;
- binlog 用 ROW,主从一致;
- 业务上"可重复读"需求少。
参考答案
- 互联网主流配置:RC + ROW 格式 binlog,理由:
1. RC 没有 Next-Key Lock 的间隙锁,并发插入不会互相阻塞,死锁少;
2. RC 下锁范围更小,锁等待和死锁概率低;
3. 业务大多是"请求级读",不需要一个事务内多次读同一张表保持一致;
4. ROW 格式 binlog 保证 RC 下主从数据一致(解决了早期 STATEMENT 的问题);
- MySQL 默认 RR 是历史兼容,新业务上线常被 DBA 改成 RC。
追问
- 问:RR 有什么场景必须用?答:主从做一致性快照备份(
mysqldump --single-transaction)靠 RR 的 ReadView 保证导出一致性。 - 问:改成 RC 会丢什么?答:一个事务内两次普通 SELECT 看到的可能不一样;如果业务代码假设"事务内读稳定"会出 bug,要 review。
题目 15:RR 级别下真的完全杜绝幻读了吗?
考点
- 快照读 vs 当前读;
- 不一致读(same transaction current read sees new rows);
- 严谨回答。
参考答案
- 严谨答案:InnoDB RR 在"当前读"场景下通过 Next-Key Lock 解决了幻读;但在"快照读 + 当前读混用"时仍会出现逻辑上的幻读:
1. 事务内第一次普通 SELECT(快照读)看不到新插入的行;
2. 事务内随后 SELECT ... FOR UPDATE(当前读)看到了新插入的行;
- 这不是 MVCC 的 bug,而是两种读机制混用的结果。要完全避免,事务内"读判断 + 写"要统一用当前读。
- 面试时主动点出这层,比背"RR 解决幻读"显得理解更深。
追问
- 问:SERIALIZABLE 下还有幻读吗?答:所有普通 SELECT 自动加 S 锁变当前读,完全串行,没有幻读。
- 问:怎么测试这个现象?答:两个会话,按题目 9 的时序操作,立刻能复现。
题目 16:怎么查看当前会话的隔离级别与锁状态?
考点
- transaction_isolation;
- performance_schema.data_locks / data_lock_waits;
- 8.0 替代 information_schema.INNODB_LOCKS。
参考答案
-- 查看隔离级别
SELECT @@transaction_isolation;
-- 8.0 查看当前锁(旧的 INNODB_LOCKS/LOCK_WAITS 已废弃)
SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;
-- 查看当前所有事务
SELECT trx_id, trx_started, trx_query
FROM information_schema.INNODB_TRX;
-- 一句话看谁堵谁
SELECT r.trx_mysql_thread_id AS blocked,
b.trx_mysql_thread_id AS blocking
FROM performance_schema.data_lock_waits w
JOIN information_schema.INNODB_TRX b ON b.trx_id = w.blocking_engine_transaction_id
JOIN information_schema.INNODB_TRX r ON r.trx_id = w.requesting_engine_transaction_id;
追问
- 问:5.7 怎么看锁?答:
SHOW ENGINE INNODB STATUS的 TRANSACTIONS 段,以及information_schema.INNODB_LOCKS/INNODB_LOCK_WAITS。 - 问:data_locks 的 LOCK_MODE 字段怎么读?答:
X= 记录排他锁;X,GAP= 间隙排他锁;X,Next-Key= Next-Key Lock;X,REC_NOT_GAP= 纯记录锁。
题目 17:串行化 SERIALIZABLE 怎么实现?什么场景用?
考点
- 普通 SELECT 变加锁读;
- 性能极差;
- 极少使用。
参考答案
- SERIALIZABLE 把事务串起来执行:所有普通 SELECT 自动转为 `LOCK IN SHARE MODE`(加 S 锁);
- 这样读写之间也互斥,彻底解决脏读/不可重复读/幻读;
- 代价:并发暴跌、死锁增多,生产几乎不用;
- 只在极少数"正确性压倒性能"的场景(如金融账户日终对账)才考虑,而且一般靠业务层加锁或分布式锁实现。
SET SESSION transaction_isolation = 'SERIALIZABLE';
START TRANSACTION;
SELECT * FROM account WHERE id = 1; -- 自动加 S 锁,其他事务写这行会阻塞
COMMIT;
追问
- 问:SERIALIZABLE 和"应用层加分布式锁"谁好?答:分布式锁粒度可控、性能好;数据库串行化把并发降到接近单线程,一般不用。
- 问:SERIALIZABLE 下还会死锁吗?答:会,而且更多,因为所有读都加锁,容易和写形成循环等待。
隔离级别速查表
| 级别 | 脏读 | 不可重复读 | 幻读 | 锁范围 | 典型用途 |
|---|---|---|---|---|---|
| READ UNCOMMITTED | 有 | 有 | 有 | 无 | 几乎不用 |
| READ COMMITTED | 无 | 有 | 有 | 记录锁 | 互联网主流 |
| REPEATABLE READ | 无 | 无 | InnoDB 解决 | Next-Key Lock | MySQL 默认、备份 |
| SERIALIZABLE | 无 | 无 | 无 | 读也加锁 | 极少用 |
| 读类型 | 语句 | 加锁 | 读版本 |
|---|---|---|---|
| 快照读 | 普通 SELECT | 无 | ReadView 历史版本 |
| 当前读 | FOR UPDATE / FOR SHARE / UPDATE / DELETE | 有 | 最新已提交 |
常见坑与注意事项
1. 以为 RR 下所有读都可重复:普通 SELECT 可重复,但 FOR UPDATE 等当前读读最新,混用会"逻辑幻读"。
2. 以为 RC 没有锁:RC 没有间隙锁,但记录锁照样加,写并发照样互斥。
3. 改了全局隔离级别就完事:已建立的连接还是老级别,连接池不重建不生效。
4. `SET TRANSACTION ISOLATION LEVEL` 写在事务里:事务内改不生效,要在 BEGIN 之前。
5. 以为唯一索引能防并发重复插入:能兜底报错,但"先 SELECT 再 INSERT"的代码路径仍会抛异常,要 catch 后转成 UPDATE。
6. RR 下大范围 SELECT FOR UPDATE:Next-Key Lock 锁一片,线上大查询直接锁表级阻塞。
7. 把 `LOCK IN SHARE MODE` 当优化:它加 S 锁反而阻塞写,多数场景该用快照读。
复习清单
| 题目 | 核心结论 | 易错点 |
|---|---|---|
| 四级隔离 | RU/RC/RR/SERIALIZABLE | 顺序别记反 |
| 默认级别 | InnoDB = RR | 答成 RC |
| 脏读 | 读未提交 | 和不可重复读混淆 |
| 不可重复读 | 同行 UPDATE 两次结果不同 | 和幻读混淆 |
| 幻读 | 范围行数变化 | 只答"RR 解决了",不说当前读 |
| ReadView | RR 首次 SELECT 生成,RC 每条 SELECT 生成 | 答成"事务启动就生成" |
| 当前读 | FOR UPDATE/UPDATE 读最新加锁 | 忘了 UPDATE 也是当前读 |
| Next-Key Lock | 记录锁 + 间隙锁,左开右闭 | 说反区间方向 |
| RC vs RR | RC 无间隙锁并发高 | 忘了 binlog 要 ROW |
| 查锁 | performance_schema.data_locks | 还在用废弃的 INNODB_LOCKS |
练习题
练 1:会话 A 在 RR 下 BEGIN; SELECT balance FROM account WHERE id=1; 看到 100。会话 B 把它改成 50 并 COMMIT。会话 A 再次普通 SELECT 看到多少?改成 SELECT ... FOR UPDATE 又看到多少?
<details>
<summary>参考答案</summary>
再次普通 SELECT 看到 100(ReadView 固定,可重复读);改成 FOR UPDATE 是当前读,看到最新已提交值 50。这就是"快照读 vs 当前读"的差异。
</details>
练 2:为什么 mysqldump --single-transaction 必须在 RR 下运行?
<details>
<summary>参考答案</summary>
--single-transaction 靠 RR 的一致性 ReadView,在导出开始时生成一个快照,整个 dump 过程都基于这个快照,保证导出的数据在时间点上一致(不会导到一半别的事务把表改了)。RC 下每条语句新 ReadView,导出来的数据跨语句就不一致了。
</details>
练 3:写一条 SQL 找出 8.0 下当前被阻塞的事务和阻塞它的事务 ID。
<details>
<summary>参考答案</summary>
SELECT r.trx_mysql_thread_id AS blocked_id,
b.trx_mysql_thread_id AS blocking_id,
r.trx_query AS blocked_sql
FROM performance_schema.data_lock_waits w
JOIN information_schema.INNODB_TRX b ON b.trx_id = w.blocking_engine_transaction_id
JOIN information_schema.INNODB_TRX r ON r.trx_id = w.requesting_engine_transaction_id;
</details>
下一篇预告:第 5 章 锁机制面试精讲
文章回复
0 条公开回复