RSS
菜单
全部文章快讯开发科技深度热点

第 4 章 隔离级别与并发问题面试精讲

内容摘要

隔离级别是事务篇的"第二战场",也是面试官最爱深挖的考点。本篇用 17 道高频题讲清四个隔离级别分别解决什么并发问题、脏读/不可重复读/幻读的区别、InnoDB 在 RR 级别下怎么用 Next-Key Lock 挡住幻读、当前读与快照读的差异,以及为什么互联网公司普遍偏爱 RC。学完本篇,你能在白板上画出两个会话的执行时序,并准确说出每一步看到的结果。前置知识:第 3 章事务与 ACID;本系列示例基于 MySQL 8.0。

📖 本篇简介:隔离级别是事务篇的"第二战场",也是面试官最爱深挖的考点。本篇用 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 章 锁机制面试精讲

— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《第 4 章 隔离级别与并发问题面试精讲》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。