事务与隔离级别
校招考察频度:★ 高频(核心必背·高频生死线) 一句话速记:PG 底层无脏读,RC 语句刷快照,RR 事务定快照;PG 无间隙锁,RR 靠快照天然防幻读;并发交叉防写偏斜,强一致靠 SSI 乐观重试。
事务隔离级别是后端高并发研发面试中最具深度和区分度的模块。许多应届生只记住了 SQL 92 标准的书本定义,但在面试官深挖底层实现与具体数据库表现时频频翻车。掌握 PostgreSQL 独特的隔离级别实现与无锁防幻读机制,是拿下大厂 Offer 的核心武器。
一、事务 ACID 四大特性与底层实现支撑
在技术面试中,不能只背名词,必须讲清每个特性由什么底层机制提供物理保障:
| 特性 | 技术定义 | PostgreSQL 底层实现机制 |
|---|---|---|
| 原子性(Atomicity) | 事务中的所有 SQL 操作要么全部成功提交,要么全部失败回滚,不可分割。 | CLOG(pg_xact 提交日志位图):事务失败时直接在 CLOG 中标记为 ABORTED;堆表中的新元组对后续快照天然不可见。 |
| 持久性(Durability) | 事务一旦提交成功,其修改就永久保存在磁盘中,即使宕机断电也不丢失。 | WAL(预写日志 Write-Ahead Logging):事务 COMMIT 时必须等待 WAL 刷盘(fsync),数据页留在内存异步慢慢刷。 |
| 隔离性(Isolation) | 多个并发事务同时执行时,各事务的读写操作互不干扰。 | MVCC(多版本并发控制) + 锁机制(Locking) + 快照算法。 |
| 一致性(Consistency) | 事务执行前后,数据库从一个合法状态转移到另一个合法状态,不破坏业务完整性约束。 | 最终目的:由原子性、持久性、隔离性共同保障,加上数据库的约束机制(主键、外键、Check 约束)。 |
二、SQL-92 四大隔离级别在 PostgreSQL 中的真实表现
SQL-92 标准定义了四种隔离级别与三大现象(脏读、不可重复读、幻读)。 但在 PostgreSQL 的内核实现中,情况有非常关键的特殊之处(大厂必考陷阱):
| 隔离级别 | 脏读(Dirty Read) | 不可重复读(Non-repeatable Read) | 幻读(Phantom Read) | PostgreSQL 生产现状 |
|---|---|---|---|---|
| 读未提交(Read Uncommitted) | ❌ 在 PG 中绝不发生! | ⚠️ 可能发生 | ⚠️ 可能发生 | PG 内部直接将其映射为 RC,PG 永远读不到未提交数据! |
| 读已提交(Read Committed, RC) | ❌ 不发生 | ⚠️ 可能发生 | ⚠️ 可能发生 | PostgreSQL 官方默认级别(并发度高,死锁极少)。 |
| 可重复读(Repeatable Read, RR) | ❌ 不发生 | ❌ 不发生 | ❌ 在 PG 中不发生! | 快照隔离(Snapshot Isolation),天然消除幻读。 |
| 可串行化(Serializable, SSI) | ❌ 不发生 | ❌ 不发生 | ❌ 不发生 | SSI(可串行化快照隔离),基于依赖图检测冲突,性能远超传统悲观锁串行化。 |
关键点解析:为什么 PG 根本没有“读未提交”?
- 因为 PostgreSQL 的 MVCC 依赖元组头的
xmin/xmax以及事务状态位图(CLOG); - 任何查询在扫描元组时,都必须通过快照判定该元组的创建事务是否已提交。未提交的事务在 CLOG 中处于
IN_PROGRESS状态,对其他任何查询都物理不可见; - 因此,即使你显式设置
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;,PostgreSQL 也会自动提升为 Read Committed。
三、RC vs RR:快照生成时机的本质差异
面试官经典灵魂拷问:“既然 RC 和 RR 都依赖快照读,为什么 RC 会出现不可重复读,而 RR 不会?”
核心秘密只有一句话:快照(Snapshot)的生成时间点与复用策略不同!
RC 与 RR 快照生成时机对比
- 读已提交(RC):每执行一条 SQL 语句,就会重新生成一个全新的快照。因此语句执行前只要其他事务提交了,新快照立刻能看到最新提交,导致多次读取同一行数值可能改变。
- 可重复读(RR):在事务执行第一条查询时生成快照,并在整个事务生命周期内自始至终复用该快照。后续无论别人怎么插入或修改,本事务看到的永远是启动那一刻的静态世界,彻底消除了不可重复读与幻读!
四、PostgreSQL 在 RR 级别下是如何消灭幻读的?
很多习惯了其他数据库的同学会背诵:“防幻读靠间隙锁(Gap Lock)”。 如果在面试 PostgreSQL 时这样回答,会直接扣分!
重要事实:PostgreSQL 没有间隙锁(Gap Lock)!
1. 为什么 PG 没有间隙锁也能在 RR 下防幻读?
- 在 PostgreSQL 的 RR 级别下,所有读操作都严格遵循单一快照隔离(Snapshot Isolation)。
- 即使其他并发事务向范围间隙内
INSERT了新行并提交,由于当前事务复用初始快照,新行的xmin晚于快照的事务水位线,该新行在当前事务的快照中被彻底屏蔽。 - 因此,在普通查询下,PG 在 RR 级别天然不存在幻读!
2. RR 下的并发写冲突:First-Committer-Wins 原则
既然没有间隙锁,如果两个并发事务在 RR 下试图更新同一行记录,会发生什么?
时序场景:
1. 事务 A (RR) 开启:SELECT * FROM users WHERE id = 1; (读取旧值)
2. 事务 B (RR) 开启:UPDATE users SET score = score + 10 WHERE id = 1;
3. 事务 A 执行更新:UPDATE users SET score = score + 20 WHERE id = 1; (被阻塞等待 B)
4. 事务 B 提交:COMMIT;
5. 此时事务 A 唤醒,但它不会直接覆盖 B 的结果,而是立刻抛出错误中断:
ERROR: could not serialize access due to concurrent update (SQLState: 40001)- 核心逻辑:PostgreSQL 遵循 先提交者胜(First-Committer-Wins) 原则。事务 A 发现自己快照看到的那一行已经被并发提交更新了,为了防止静默覆盖破坏可重复读的一致性视图,强制回滚报错,提示应用层重新发起事务。
五、RR 级别的阿喀琉斯之踵:写偏斜(Write Skew)异常
面试官进阶深度:“既然 RR 级别既没有不可重复读,快照又挡住了幻读,为什么还需要串行化(Serializable)级别?”
答案是:快照隔离无法解决“写偏斜(Write Skew)”异常!
经典案例:医生值班系统(Doctor on Call)
- 业务约束:医院规定,任何时刻系统内必须至少保留 1 名值班医生。
- 当前状态:医生 Alice 和 Bob 都在值班(共 2 人在岗)。
- 此时 Alice 和 Bob 同时申请请假退出值班:
并发时序:
1. 事务 1 (Alice 的请求, RR 级别):
- 查询当前值班人数:SELECT count(*) FROM doctors WHERE on_call = true; (返回 2)
- 2 > 1,符合业务约束!Alice 可以请假。
- 执行更新:UPDATE doctors SET on_call = false WHERE name = 'Alice';
2. 事务 2 (Bob 的请求, RR 级别并发执行):
- 查询当前值班人数:SELECT count(*) FROM doctors WHERE on_call = true; (快照隔离,同样返回 2)
- 2 > 1,符合业务约束!Bob 也可以请假。
- 执行更新:UPDATE doctors SET on_call = false WHERE name = 'Bob';
3. 事务 1 提交:COMMIT; (成功,因为修改的是 Alice 的行)
4. 事务 2 提交:COMMIT; (成功,因为修改的是 Bob 的行,两人修改不同行,不触发行冲突!)- 灾难结果:两个事务都成功提交,但此时医院在岗医生人数变成了 0!彻底击穿了业务完整性约束!
- 本质分析:两个事务的读取依赖(条件
count >= 2)与各自的写入(不同行)之间存在交叉依赖关系,快照隔离无法识别这种跨行的约束冲突。
六、终极解决方案:SSI(可串行化快照隔离)
传统数据库为了解决写偏斜,在 Serializable 级别下会动用沉重的读共享锁或表级锁,并发性能暴跌。 而 PostgreSQL 实现了学术界前沿的 SSI(Serializable Snapshot Isolation):
1. 核心工作原理
- 依然采用快照读:读写完全不互相阻塞,运行速度与 RR 快照隔离完全一致;
- SIREAD 锁(无阻塞意向标记):事务读取数据时,在内存中留下轻量的 SIREAD 锁(仅用于在图结构中记录“我读过哪一行/哪一页”,根本不阻塞别人的写入);
- 动态有向图检测(Cycle Detection):内核后台维护读写依赖图(Dependency Graph)。如果检测到出现环路(即
T1 -> T2 -> T1的冲突环,意味着可能发生了写偏斜): - 主动干预:SSI 引擎会挑出其中一个事务并主动中止(Abort),抛出:
ERROR: could not serialize access due to read/write dependencies among transactions - 应用层最佳实践:在微服务中使用 SSI 隔离级别时,应用层代码必须配置自动重试拦截器(捕获 SQLState
40001后休眠几毫秒并自动重跑事务)。
七、高频面试黄金问答
- 为什么互联网生产系统通常采用 RC 作为默认隔离级别?
- 吞吐与延迟最优:RC 下锁持有时间极短,没有 First-Committer-Wins 的强制回滚中断,并发写入吞吐极高;
- 死锁概率极低:由于没有间隙锁机制,死锁几率大大降低。
- 在 RC 隔离级别下,如何防止并发超卖(例如扣库存)?
- 绝对不要“先 SELECT 查库存,再在应用层判断,最后 UPDATE”;
- 应当使用原子更新带条件:
UPDATE goods SET stock = stock - 1 WHERE id = 100 AND stock >= 1; - 检查返回值受影响行数(affected rows),若为 0 说明库存不足,保证绝对并发安全。