跳到主要内容

隔离级别

问题

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 增强
PostgreSQLRead Committed (RC)SSI 可串行化无锁方案
OracleRead Committed (RC)只支持 RC 和 Serializable
SQL ServerRead Committed (RC)支持快照隔离(类似 MVCC)
MongoDBRead Committed4.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 第二次读到的值原因
RC500事务 B 已提交,新的 Read View 看到了
RR1000复用事务开始时的 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,原因:

  1. RC 下锁粒度更小(不加间隙锁),死锁概率更低
  2. RC 下 semi-consistent read 减少锁等待
  3. 大部分业务不需要可重复读的语义

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?

答案

  1. 减少死锁:RC 不加间隙锁,只有行锁,锁冲突少
  2. 减少锁等待:RC 的 semi-consistent read 可以读到已提交的最新版本再判断是否冲突
  3. 更好的并发:锁粒度更细,并发度更高
  4. 配合 ROW binlog:解决了早期 STATEMENT 格式在 RC 下的主从不一致问题
  5. 业务适配:大部分业务逻辑不依赖可重复读语义,用 RC 足够

相关链接