隔离级别
问题
SQL 标准定义了哪四种事务隔离级别?它们分别解决了什么并发问题?MySQL 和 PostgreSQL 的默认隔离级别分别是什么?
答案
并发事务的问题
| 问题 | 描述 | 示例 |
|---|---|---|
| 脏读 | 读到其他事务 未提交 的数据 | 事务 B 读到事务 A 尚未 COMMIT 的修改 |
| 不可重复读 | 同一事务内两次读 同一行,结果不同 | 事务 A 两次 SELECT 之间,事务 B 修改并提交了该行 |
| 幻读 | 同一事务内两次读 同一范围,行数不同 | 事务 A 两次 SELECT 之间,事务 B 插入了新行 |
四种隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
|---|---|---|---|---|
| 读未提交(Read Uncommitted) | ❌ 可能 | ❌ 可能 | ❌ 可能 | 最高 |
| 读已提交(Read Committed, RC) | ✅ 避免 | ❌ 可能 | ❌ 可能 | 高 |
| 可重复读(Repeatable Read, RR) | ✅ 避免 | ✅ 避免 | ❌ 可能* | 中 |
| 可串行化(Serializable) | ✅ 避免 | ✅ 避免 | ✅ 避免 | 最低 |
MySQL RR 与幻读
SQL 标准说 RR 不能避免幻读,但 InnoDB 的 RR 在大多数情况下可以避免幻读:
- 快照读(普通 SELECT):通过 MVCC 的 Read View 避免幻读
- 当前读(SELECT ... FOR UPDATE):通过 Next-Key Lock(行锁 + 间隙锁)避免幻读
但仍有特殊场景会出现"类幻读"现象。
各数据库的默认隔离级别
| 数据库 | 默认隔离级别 | 说明 |
|---|---|---|
| MySQL (InnoDB) | Repeatable Read (RR) | 通过 MVCC + Next-Key Lock 增强 |
| PostgreSQL | Read Committed (RC) | SSI 可串行化无锁方案 |
| Oracle | Read Committed (RC) | 只支持 RC 和 Serializable |
| SQL Server | Read Committed (RC) | 支持快照隔离(类似 MVCC) |
| MongoDB | Read Committed | 4.0+ 支持多文档事务 |
各隔离级别的实现原理
读未提交(Read Uncommitted)
- 直接读取数据的最新版本,不使用 MVCC 快照
- 写操作仍需要加排他锁
- 几乎不使用,因为脏读问题严重
读已提交(Read Committed)
- 每次 SELECT 都创建新的 Read View
- 所以两次 SELECT 之间如果有其他事务提交了修改,第二次能看到
可重复读(Repeatable Read)
- 事务内第一次 SELECT 创建 Read View,后续复用
- 无论其他事务如何提交,对当前事务不可见
- InnoDB 还通过 Next-Key Lock 防止幻读
可串行化(Serializable)
- 读加共享锁,写加排他锁
- 所有读写操作串行执行
- 最安全但并发度最低
Read Committed vs Repeatable Read 详细对比
-- 假设表 accounts: id=1, balance=1000
-- 事务 A -- 事务 B
BEGIN; BEGIN;
SELECT balance FROM accounts
WHERE id = 1;
-- 结果: 1000
UPDATE accounts SET balance = 500
WHERE id = 1;
COMMIT;
SELECT balance FROM accounts
WHERE id = 1;
| 隔离级别 | 事务 A 第二次读到的值 | 原因 |
|---|---|---|
| RC | 500 | 事务 B 已提交,新的 Read View 看到了 |
| RR | 1000 | 复用事务开始时的 Read View,看不到 B 的修改 |
MySQL RR 下的"幻读"边界情况
-- 事务 A -- 事务 B
BEGIN; BEGIN;
SELECT * FROM users WHERE age = 25;
-- 结果: 2 行
INSERT INTO users (name, age)
VALUES ('新用户', 25);
COMMIT;
-- 快照读:不会看到新行(MVCC 保护)
SELECT * FROM users WHERE age = 25;
-- 结果: 仍然 2 行 ✅
-- 但如果执行当前读(写操作):
UPDATE users SET name = 'xxx' WHERE age = 25;
-- 影响了 3 行 !! 包括事务 B 插入的行
-- 再次快照读:
SELECT * FROM users WHERE age = 25;
-- 结果: 3 行 !! 幻读出现了
RR 下的"快照读转当前读"幻读
InnoDB RR 下,如果事务先做快照读再做当前读(UPDATE/DELETE/SELECT FOR UPDATE),会触发"快照更新",导致后续能看到其他事务插入的行。
预防方法:在第一次查询时就使用 SELECT ... FOR UPDATE(当前读),这会加 Next-Key Lock 阻止其他事务插入。
设置隔离级别
-- MySQL
-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- 设置会话级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置全局级别
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 在配置文件中设置
-- [mysqld]
-- transaction-isolation = READ-COMMITTED
-- PostgreSQL
-- 查看
SHOW default_transaction_isolation;
-- 设置
SET default_transaction_isolation = 'repeatable read';
-- 或在事务中
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
常见面试问题
Q1: 脏读、不可重复读、幻读的区别?
答案:
| 问题 | 涉及数据 | 涉及操作 | 核心区别 |
|---|---|---|---|
| 脏读 | 同一行 | 读到未提交的修改 | 数据可能被回滚 |
| 不可重复读 | 同一行 | 两次读到不同值 | 被其他事务 UPDATE |
| 幻读 | 一个范围 | 两次读到不同行数 | 被其他事务 INSERT/DELETE |
Q2: MySQL 默认隔离级别是什么?为什么选择 RR?
答案:
MySQL InnoDB 默认隔离级别是 可重复读(RR)。
历史原因:MySQL 早期的 binlog 格式是 STATEMENT(记录 SQL 语句),在 RC 下主从复制可能因为 SQL 执行顺序不同导致数据不一致。RR + Next-Key Lock 可以避免这个问题。
现在 binlog 格式默认是 ROW(记录数据变更),RC 下主从复制也不会有问题了。很多互联网公司在生产环境中将 MySQL 隔离级别设为 RC,原因:
- RC 下锁粒度更小(不加间隙锁),死锁概率更低
- RC 下 semi-consistent read 减少锁等待
- 大部分业务不需要可重复读的语义
Q3: PostgreSQL 为什么默认用 RC 而不是 RR?
答案:
PostgreSQL 认为 RC 是最常用且性能最好的隔离级别,适合大多数业务场景。PostgreSQL 的 MVCC 实现不依赖 undo log(使用多版本元组),RC 下不需要间隙锁,并发度更高。
同时 PostgreSQL 提供了 SSI(Serializable Snapshot Isolation) 级别,用乐观并发控制替代锁,在需要强一致性时比传统的可串行化性能更好。
Q4: 可重复读和可串行化有什么区别?
答案:
| 方面 | 可重复读(RR) | 可串行化(Serializable) |
|---|---|---|
| 幻读 | 可能(InnoDB 大部分场景避免) | 完全避免 |
| 锁 | 行锁 + 间隙锁 | 读也加共享锁 |
| 并发度 | 较高 | 很低 |
| 实现 | MVCC + 锁 | 强制串行或 SSI |
| 适用 | 大部分 OLTP | 金融等强一致性场景 |
Q5: 互联网公司为什么常把 MySQL 改为 RC?
答案:
- 减少死锁:RC 不加间隙锁,只有行锁,锁冲突少
- 减少锁等待:RC 的 semi-consistent read 可以读到已提交的最新版本再判断是否冲突
- 更好的并发:锁粒度更细,并发度更高
- 配合 ROW binlog:解决了早期 STATEMENT 格式在 RC 下的主从不一致问题
- 业务适配:大部分业务逻辑不依赖可重复读语义,用 RC 足够