返回首页

15|隔离级别与并发问题

适用版本:MySQL 8.0.x(本文示例基于 InnoDB 存储引擎)

一、背景

只要数据库存在并发访问,就一定会面临这样的问题:

  • 多个事务同时读写同一批数据时,结果是否可靠?
  • 当前事务读到的数据,是其他事务已经提交的结果,还是尚未提交的临时结果?
  • 同一条查询在一个事务里执行两次,为什么结果可能不一样?

这些现象都与事务隔离级别密切相关。

很多人刚接触事务时,会先记住四个隔离级别的名字,但真正写业务 SQL 时,最容易踩坑的并不是“名词记不住”,而是:

  • 不知道各种并发问题到底是怎么发生的;
  • 不清楚 MySQL 8.0 默认隔离级别为何是 REPEATABLE READ
  • 不明白 MVCC、当前读、锁定读与隔离级别之间的关系。

这篇文章就围绕这些问题,做一篇偏教程、偏学习笔记风格的梳理。


二、并发问题到底在解决什么

在没有隔离机制时,多个事务可以任意交错执行。这样虽然并发高,但很容易出现数据不可靠的情况。

经典的并发问题通常有三类:

  1. 脏读(Dirty Read)
  2. 不可重复读(Non-repeatable Read)
  3. 幻读(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

同一事务里,两次读取同一行却得到了不同值,这就是不可重复读。

关键点

  • 关注的是“同一行数据的值变了”;
  • 通常和 UPDATEDELETE 等已提交修改有关;
  • 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 标准定义了四种事务隔离级别:

  1. READ UNCOMMITTED
  2. READ COMMITTED
  3. REPEATABLE READ
  4. SERIALIZABLE

隔离级别越高,事务之间越“看不见”彼此;但同时并发能力一般也会下降。


五、核心机制:各隔离级别的行为差异

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 UPDATEUPDATEDELETE)会读取最新版本,并结合锁机制防止范围内并发插入。

因此,讨论“是否会发生幻读”时,不能只背标准结论,还要结合读类型来看。


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 UPDATE
  • SELECT ... FOR SHARE
  • UPDATE
  • DELETE

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 READREAD 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 的一致性读;
  • 当前读与锁定读;
  • 不同业务对一致性与性能的取舍。

理解这些之后,再学习锁机制、间隙锁、死锁排查,就会自然很多。


📝 版权声明:本文为原创技术博客,转载请注明出处。

如文章中存在错误或不准确之处,欢迎在评论区指正,感谢您的阅读与支持!

上一篇

14 |事务与 ACID 特性

下一篇

16 | InnoDB 锁机制详解