适用版本:MySQL 8.0.x(本文示例基于 InnoDB 存储引擎)
一、背景
只要数据库存在并发访问,就一定会面临这样的问题:
- 多个事务同时读写同一批数据时,结果是否可靠?
- 当前事务读到的数据,是其他事务已经提交的结果,还是尚未提交的临时结果?
- 同一条查询在一个事务里执行两次,为什么结果可能不一样?
这些现象都与事务隔离级别密切相关。
很多人刚接触事务时,会先记住四个隔离级别的名字,但真正写业务 SQL 时,最容易踩坑的并不是“名词记不住”,而是:
- 不知道各种并发问题到底是怎么发生的;
- 不清楚 MySQL 8.0 默认隔离级别为何是
REPEATABLE READ; - 不明白 MVCC、当前读、锁定读与隔离级别之间的关系。
这篇文章就围绕这些问题,做一篇偏教程、偏学习笔记风格的梳理。
二、并发问题到底在解决什么
在没有隔离机制时,多个事务可以任意交错执行。这样虽然并发高,但很容易出现数据不可靠的情况。
经典的并发问题通常有三类:
- 脏读(Dirty Read)
- 不可重复读(Non-repeatable Read)
- 幻读(Phantom Read)
理解隔离级别,必须先把这三个问题看懂。
三、核心机制:三类并发问题
3.1 脏读
脏读指的是:
一个事务读取到了另一个事务尚未提交的数据。
这类数据之所以叫“脏”,是因为对方事务后续可能会回滚。一旦回滚,当前事务之前读到的数据其实就是“无效数据”。
示例
先准备测试表:
CREATE TABLE account (
id INT PRIMARY KEY,
balance INT NOT NULL
);
INSERT INTO account VALUES (1, 1000);
事务 A:
START TRANSACTION;
UPDATE account SET balance = 500 WHERE id = 1;
-- 暂不提交
事务 B:
SELECT balance FROM account WHERE id = 1;
如果事务 B 此时读到了 500,而事务 A 随后执行:
ROLLBACK;
那么事务 B 之前读到的 500 就是一次脏读。
关键点
- 脏读最严重,因为读到了本不该存在的“临时值”;
- 在 MySQL InnoDB 中,只要不是
READ UNCOMMITTED,就不会发生脏读。
3.2 不可重复读
不可重复读指的是:
在同一个事务中,多次读取同一行记录,结果却不一致。
它通常是因为另一个事务在中途提交了对该行的修改。
示例
事务 A:
START TRANSACTION;
SELECT balance FROM account WHERE id = 1;
-- 第一次读到 1000
事务 B:
START TRANSACTION;
UPDATE account SET balance = 1200 WHERE id = 1;
COMMIT;
事务 A 再次读取:
SELECT balance FROM account WHERE id = 1;
-- 第二次读到 1200
同一事务里,两次读取同一行却得到了不同值,这就是不可重复读。
关键点
- 关注的是“同一行数据的值变了”;
- 通常和
UPDATE、DELETE等已提交修改有关; REPEATABLE READ以及更高隔离级别可以避免普通一致性读下的不可重复读。
3.3 幻读
幻读指的是:
在同一个事务中,按照相同条件查询多次,发现结果集中的“行数”发生变化。
也就是说,变化的不是某一行的值,而是“符合条件的记录集合”。
示例
准备表:
CREATE TABLE orders (
id INT PRIMARY KEY,
amount INT NOT NULL
);
INSERT INTO orders VALUES (1, 100), (2, 300);
事务 A:
START TRANSACTION;
SELECT COUNT(*) FROM orders WHERE amount >= 200;
-- 结果为 1
事务 B:
START TRANSACTION;
INSERT INTO orders VALUES (3, 500);
COMMIT;
事务 A 再查一次:
SELECT COUNT(*) FROM orders WHERE amount >= 200;
-- 结果变为 2
这类“像凭空多出来一行”的现象,就叫幻读。
关键点
- 关注的是“结果集范围变化”;
- 常由
INSERT导致; - 在 MySQL InnoDB 中,普通快照读与锁定读对幻读的表现并不完全一样,要结合 MVCC 和 next-key lock 一起理解。
四、四种隔离级别
SQL 标准定义了四种事务隔离级别:
READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLE
隔离级别越高,事务之间越“看不见”彼此;但同时并发能力一般也会下降。
五、核心机制:各隔离级别的行为差异
5.1 READ UNCOMMITTED:读未提交
这是最低的隔离级别。
在该级别下:
- 可以读到别的事务尚未提交的数据;
- 因此可能出现脏读、不可重复读、幻读。
设置方式
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
使用评价
理论上并发高,但实际业务几乎很少使用,因为读到脏数据的代价太大。
5.2 READ COMMITTED:读已提交
该级别要求:
一个事务只能读到其他事务已经提交的数据。
因此:
- 能避免脏读;
- 但仍可能出现不可重复读和幻读。
特征理解
在 READ COMMITTED 下,每次普通 SELECT 都会读取当时最新已提交版本。这就意味着:
- 同一个事务里第一次查询和第二次查询,可能看到不同结果;
- 因为两次读取之间,别的事务可能已经提交了新版本。
典型应用
不少数据库系统默认使用 READ COMMITTED。它在一致性与并发性能之间做了一个相对均衡的折中。
5.3 REPEATABLE READ:可重复读
这是 MySQL InnoDB 的默认隔离级别。
它的核心目标是:
在同一个事务中,多次读取同一份数据,结果应保持一致。
在 InnoDB 中,REPEATABLE READ 通过 MVCC 实现普通快照读的一致性,因此:
- 可以避免脏读;
- 可以避免普通一致性读下的不可重复读;
- 对幻读的控制比很多人想象中更复杂。
为什么说“更复杂”?
因为在 InnoDB 里:
- 普通
SELECT通常是快照读,读取事务启动时可见的数据版本; - 锁定读 / 当前读(如
SELECT ... FOR UPDATE、UPDATE、DELETE)会读取最新版本,并结合锁机制防止范围内并发插入。
因此,讨论“是否会发生幻读”时,不能只背标准结论,还要结合读类型来看。
5.4 SERIALIZABLE:串行化
这是最高隔离级别。
它要求并发事务的执行效果尽可能等价于“一个一个串行执行”。
特点:
- 能避免脏读、不可重复读、幻读;
- 会引入更多锁冲突;
- 并发性能通常最差。
适用场景
只有在极少数对一致性要求极高、且可以接受明显性能下降的场景下,才会考虑使用。
六、MySQL 8.0 中隔离级别的查看与设置
6.1 查看当前会话隔离级别
SELECT @@transaction_isolation;
6.2 设置当前会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
6.3 设置全局隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
注意:
SESSION只影响当前连接;GLOBAL影响新连接,不影响已经建立的会话;- 生产环境修改全局隔离级别前,一定要评估业务读写行为。
七、示例:MySQL 默认可重复读是如何工作的
我们用一个例子理解快照读。
准备数据:
CREATE TABLE product (
id INT PRIMARY KEY,
stock INT NOT NULL
);
INSERT INTO product VALUES (1, 10);
事务 A:
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT stock FROM product WHERE id = 1;
-- 读到 10
事务 B:
START TRANSACTION;
UPDATE product SET stock = 20 WHERE id = 1;
COMMIT;
事务 A 再次查询:
SELECT stock FROM product WHERE id = 1;
-- 仍然读到 10
这说明事务 A 读的是自己事务开始时的可见版本,而不是对方刚提交的新值。
但如果事务 A 改成:
SELECT * FROM product WHERE id = 1 FOR UPDATE;
它就不再是普通快照读,而是当前读,行为会不同。
八、快照读、当前读与隔离级别的关系
理解 MySQL 隔离级别时,一个很重要的知识点是:不是所有读取都是同一种“读”。
8.1 快照读
快照读通常指普通 SELECT。
特点:
- 基于 MVCC;
- 不一定加锁;
- 读取的是某个一致性视图中的版本。
8.2 当前读
当前读会读取记录的最新版本,并在必要时加锁。
常见语句包括:
SELECT ... FOR UPDATESELECT ... FOR SHAREUPDATEDELETE
8.3 为什么这个区别重要
因为很多文章简单说“可重复读解决了幻读”,但在 MySQL InnoDB 里,实际更准确的表达应该是:
- 对普通快照读,事务看到的是固定一致性视图,因此结果集通常稳定;
- 对锁定读 / 当前读,InnoDB 还会借助 next-key lock 等机制抑制范围内并发插入,从而避免实际业务中的幻读问题。
九、常见问题
9.1 MySQL 默认为什么不是 READ COMMITTED?
因为 InnoDB 选择了 REPEATABLE READ 作为默认级别,以便在大量 OLTP 场景里提供更稳定的一致性读体验,并结合 MVCC 与间隙锁机制控制并发问题。
9.2 隔离级别越高越好吗?
不是。
隔离越高,冲突越多、吞吐量越低。隔离级别的选择本质上是在:
- 数据一致性要求;
- 业务可接受的异常;
- 系统并发性能;
之间做平衡。
9.3 使用 REPEATABLE READ 后是不是完全不会幻读?
不能机械地这样理解。
在 InnoDB 中:
- 对普通一致性读,事务通常看到固定快照;
- 对当前读,需要借助 next-key lock 等锁机制控制范围内变化;
- 不同 SQL 形式下,观察到的现象会不同。
所以学习幻读时,不要只记结论,更要关注具体 SQL 是快照读还是当前读。
9.4 查询变慢是不是隔离级别太高导致的?
有可能,但不能直接下结论。
查询慢还可能与以下问题有关:
- 缺少索引;
- 扫描范围太大;
- 长事务导致版本链过长;
- 锁等待严重;
- 执行计划不佳。
隔离级别只是影响因素之一。
十、实践建议
10.1 优先理解业务是否允许“读旧数据”
如果业务能够接受短暂读到旧版本数据,那么 REPEATABLE READ 或 READ COMMITTED 都是可选方案;关键在于读写模型是否清楚。
10.2 默认不要轻易使用 SERIALIZABLE
除非你非常明确要用“串行化”语义,否则不要贸然提高到最高隔离级别,因为锁冲突和性能开销通常很明显。
10.3 区分普通查询与锁定查询
业务代码中要明确:
- 普通查询是否只是展示;
- 是否需要读取最新值;
- 是否要防止别人同时修改。
如果需要“读完立刻准备更新”,往往应考虑 FOR UPDATE 这类当前读,而不是只写普通 SELECT。
10.4 重要业务不要只依赖隔离级别兜底
例如扣库存、扣余额时,除了事务隔离级别,还应结合:
- 合理索引;
- 条件更新;
- 唯一约束;
- 幂等设计;
- 重试策略。
10.5 排查并发异常时,先确认会话级别
不同连接池、不同程序模块,可能设置了不同会话隔离级别。排查问题时要先确认:
SELECT @@transaction_isolation;
不要默认所有连接都与全局设置一致。
十一、小结
事务隔离级别的本质,是数据库为并发事务之间的“可见性”和“互相影响程度”制定的规则。
可以用一句话快速概括:
READ UNCOMMITTED:可能读到未提交数据,问题最多;READ COMMITTED:避免脏读,但仍会不可重复读;REPEATABLE READ:MySQL 默认级别,结合 MVCC 提供稳定一致性读;SERIALIZABLE:最严格,但并发性能通常最差。
真正把隔离级别学透,关键不只是背定义,而是把下面几件事串起来:
- 并发问题的表现;
- MVCC 的一致性读;
- 当前读与锁定读;
- 不同业务对一致性与性能的取舍。
理解这些之后,再学习锁机制、间隙锁、死锁排查,就会自然很多。
📝 版权声明:本文为原创技术博客,转载请注明出处。
如文章中存在错误或不准确之处,欢迎在评论区指正,感谢您的阅读与支持!