事务隔离级别与并发问题
本教程共 46 篇 · 第 38 篇 · 更新于 2026-07-30 · 约 14 分钟阅读
38. 事务隔离级别与并发问题
本节目标:搞清楚脏读、不可重复读、幻读三种并发问题的表现和原因,掌握四种隔离级别的区别和选择,理解 MVCC 多版本并发控制的基本原理。
38.1 并发事务的问题
当多个事务同时对同一份数据操作时,如果不做隔离,就会出问题。MySQL 定义了三种经典的并发问题。
脏读(Dirty Read)
事务 A 读到了事务 B 尚未提交的修改。如果 B 后来回滚了,A 读到的就是”脏”数据。
时间线 事务A 事务B
1 BEGIN;
2 UPDATE users SET age = 99 WHERE id = 1;
3 SELECT age FROM users WHERE id = 1;
-- 读到了 age = 99(B 还没提交)
4 ROLLBACK;
5 -- age 实际还是原值,A 之前读到的 99 是脏数据
脏读是最严重的问题,读到的是根本不存在的数据。
不可重复读(Non-repeatable Read)
事务 A 两次读同一行数据,结果不一样,因为事务 B 在中间修改并提交了。
时间线 事务A 事务B
1 BEGIN;
2 SELECT age FROM users WHERE id = 1;
-- age = 25
3 BEGIN;
4 UPDATE users SET age = 30 WHERE id = 1;
5 COMMIT;
6 SELECT age FROM users WHERE id = 1;
-- age = 30(和上次读的不一样了)
7 COMMIT;
和脏读的区别:脏读读到的是未提交的数据,不可重复读读到的是已提交的数据。但问题在于,事务 A 在同一个事务里两次读同一行结果不一致,破坏了”可重复读”的预期。
幻读(Phantom Read)
事务 A 两次执行同一条件查询,结果集行数不一样,因为事务 B 在中间插入并提交了新行。
时间线 事务A 事务B
1 BEGIN;
2 SELECT * FROM users WHERE age > 20;
-- 假设查到 3 行
3 BEGIN;
4 INSERT INTO users(username, age) VALUES('new', 25);
5 COMMIT;
6 SELECT * FROM users WHERE age > 20;
-- 查到 4 行(多了一条"幻影"行)
7 COMMIT;
和不可重复读的区别:不可重复读是同一行数据被改了,幻读是行数变了(有新行插入或旧行删除)。
三种问题对比
| 问题 | 原因 | 读到的是 | 严重程度 |
|---|---|---|---|
| 脏读 | 读未提交的修改 | 不存在的脏数据 | 最严重 |
| 不可重复读 | 读已提交的修改 | 已提交但前后不一致 | 中等 |
| 幻读 | 读已提交的新插入/删除 | 行数变化 | 较轻 |
38.2 四种隔离级别
SQL 标准定义了四种隔离级别,级别越高隔离性越强,但并发性能越低。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
|---|---|---|---|---|
| READ UNCOMMITTED(读未提交) | 可能 | 可能 | 可能 | 最高 |
| READ COMMITTED(读提交) | 防止 | 可能 | 可能 | 高 |
| REPEATABLE READ(可重复读) | 防止 | 防止 | 可能* | 中 |
| SERIALIZABLE(串行化) | 防止 | 防止 | 防止 | 最低 |
*MySQL InnoDB 的 REPEATABLE READ 级别通过临键锁(Next-key Lock)也能防止幻读,这是 InnoDB 的增强实现,不是 SQL 标准的要求。
MySQL 默认隔离级别是 REPEATABLE READ(可重复读)。
-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- 结果:REPEATABLE-READ
-- MySQL 8.0 之前用 @@tx_isolation(已废弃)
设置隔离级别
-- 设置全局隔离级别
SET GLOBAL transaction_isolation = 'READ-COMMITTED';
-- 设置当前会话隔离级别
SET SESSION transaction_isolation = 'READ-COMMITTED';
-- 只对下一个事务生效
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
| 设置范围 | 语法 | 生效范围 |
|---|---|---|
| 全局 | SET GLOBAL | 所有新连接 |
| 会话 | SET SESSION | 当前连接 |
| 单事务 | SET TRANSACTION | 下一个事务 |
38.3 各级别详解
READ UNCOMMITTED(读未提交)
最低级别,事务可以读到其他事务未提交的数据。基本不用,了解即可。
READ COMMITTED(读提交)
只能读到其他事务已提交的数据。防止了脏读,但不可重复读和幻读仍会发生。
Oracle 和 PostgreSQL 默认用这个级别。
-- 演示不可重复读
SET SESSION transaction_isolation = 'READ-COMMITTED';
-- 事务 A
BEGIN;
SELECT age FROM users WHERE id = 1; -- 假设 age = 25
-- 事务 B(另一个会话)
BEGIN;
UPDATE users SET age = 30 WHERE id = 1;
COMMIT;
-- 事务 A 再查
SELECT age FROM users WHERE id = 1; -- age = 30,和之前不一样
COMMIT;
REPEATABLE READ(可重复读)— MySQL 默认
同一事务内多次读同一行,结果一致。InnoDB 通过 MVCC 实现这一点。
-- 事务 A
SET SESSION transaction_isolation = 'REPEATABLE-READ';
BEGIN;
SELECT age FROM users WHERE id = 1; -- age = 25
-- 事务 B
BEGIN;
UPDATE users SET age = 30 WHERE id = 1;
COMMIT;
-- 事务 A 再查
SELECT age FROM users WHERE id = 1; -- age = 25,没变!
COMMIT;
NoteInnoDB 在 REPEATABLE READ 下不仅防止了不可重复读,还通过临键锁防止了幻读。这是 InnoDB 比标准 SQL 做得更严格的地方。下一节锁机制会详细讲临键锁。
SERIALIZABLE(串行化)
最高级别,所有事务串行执行,完全隔离。性能最差,基本不用于生产环境。
SET SESSION transaction_isolation = 'SERIALIZABLE';
-- 此时所有 SELECT 会自动加共享锁,其他事务无法修改被查的行
38.4 MVCC 简介
MVCC(Multi-Version Concurrency Control,多版本并发控制) 是 InnoDB 实现高并发读写的核心机制。
核心思想
每行数据维护多个版本。事务读取时,根据隔离级别选择”看哪个版本”,不需要加锁就能读到一致的数据。
实现要素
1. 隐藏列
InnoDB 给每行数据加了两个隐藏列:
DB_TRX_ID:最后修改这行的事务 ID;DB_ROLL_PTR:指向 undo log 中该行的历史版本。
2. undo log 版本链
每次修改一行数据,旧版本被记录在 undo log 中,通过 DB_ROLL_PTR 串成一条链。这就是这行数据的”历史版本链”。
当前数据行 (trx_id=200)
-> undo log: 旧版本 (trx_id=150)
-> undo log: 更旧版本 (trx_id=100)
3. Read View(读视图)
事务执行查询时,会创建一个 Read View,记录”当前还有哪些事务活跃”。然后沿着版本链找,找到第一个对当前事务可见的版本。
- READ COMMITTED:每次 SELECT 都创建新的 Read View,所以能看到最新已提交的数据;
- REPEATABLE READ:事务第一次 SELECT 时创建 Read View,之后一直复用,所以看到的数据永远是事务开始时的快照。
REPEATABLE READ 下:
事务开始 -> 创建 Read View(记录当前活跃事务列表)
每次查询 -> 用同一个 Read View 判断版本可见性
结果 -> 整个事务期间看到的数据一致
TipMVCC 只在普通 SELECT(快照读)中生效。UPDATE、DELETE、
SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE这些是当前读,直接读最新数据并加锁,不走 MVCC。
快照读 vs 当前读
| 类型 | 说明 | 语句 |
|---|---|---|
| 快照读 | 读 MVCC 版本链中的可见版本 | 普通 SELECT |
| 当前读 | 读最新数据并加锁 | SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、INSERT、UPDATE、DELETE |
-- 快照读:走 MVCC,不加锁
SELECT * FROM users WHERE id = 1;
-- 当前读:读最新数据,加行锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;
38.5 隔离级别选择建议
| 场景 | 建议级别 | 原因 |
|---|---|---|
| 一般业务 | REPEATABLE READ(默认) | MySQL 默认,兼顾一致性和性能 |
| 高并发、允许不一致 | READ COMMITTED | 锁冲突少,并发高 |
| 严格一致性要求 | SERIALIZABLE | 完全隔离,但性能差 |
Note大多数互联网公司用默认的 REPEATABLE READ。少数对并发要求极高的场景(如秒杀)会用 READ COMMITTED 减少锁冲突。SERIALIZABLE 几乎不会在生产中使用。
38.6 小结
- 三种并发问题:脏读(读未提交)、不可重复读(同一行前后不一致)、幻读(行数变化);
- 四种隔离级别从低到高:READ UNCOMMITTED -> READ COMMITTED -> REPEATABLE READ -> SERIALIZABLE;
- MySQL 默认 REPEATABLE READ,InnoDB 在此级别下也能防止幻读;
- MVCC 通过版本链和 Read View 实现快照读,不加锁也能保证一致性读;
- RC 每次查询创建新 Read View,RR 整个事务复用一个 Read View;
- 快照读走 MVCC,当前读加锁读最新数据。
下一节讲锁机制,深入理解 InnoDB 的行锁、间隙锁和死锁。