人人都会AI编程

6.2 事务隔离级别

更新时间:2026-07-10

多个事务同时读写同一份数据时,如果没有任何隔离措施,就会出现各种奇怪的数据问题。SQL 标准定义了四种隔离级别,由低到高分别是:读未提交、读已提交、可重复读、串行化。隔离级别越高,数据一致性越强,但并发性能越低。

MySQL 的 InnoDB 引擎对这四种级别全部支持,并且通过 MVCC(多版本并发控制)锁机制 的组合来实现。理解每个级别的行为、解决的问题和留下的隐患,是写出正确事务代码的前提。


6.2.1 读未提交(Read Uncommitted)

定义:一个事务可以读到其他事务尚未提交的修改。

这是隔离级别最低的一档,几乎没有任何并发控制。它的主要问题是 脏读:事务 A 修改了一行数据但还未提交,事务 B 就能读到这个修改后的值。如果事务 A 随后回滚,那么事务 B 读到的就是一个从未真正存在过的数据,基于它做的后续判断全部是错的。

脏读示例

| 时间 | 事务 A(余额扣减) | 事务 B(读取余额) |
|------|---------------------|----------------------|
| T1 | UPDATE 账户 SET 余额 = 余额 - 100 WHERE id = 1 | |
| T2 | | SELECT 余额 FROM 账户 WHERE id = 1 → 看到扣减后的值 |
| T3 | ROLLBACK | |

事务 B 在 T2 读到的余额是事务 A 尚未提交的中间状态。如果此时业务逻辑以为扣减已生效,就可能出现错误决策。

实现方式:InnoDB 在读未提交级别下,读操作不加锁,直接读取数据行当前的最新版本,完全无视事务提交状态。

实际使用:基本不会用于生产环境。只有在某些极端场景(比如仅用于粗略统计、允许误差的报表)才会有极少数人铤而走险。任何对数据一致性有要求的系统都应避免使用。


6.2.2 读已提交(Read Committed)

定义:一个事务只能读到其他事务已经提交的修改。

读已提交解决了脏读问题。事务 B 只能看到事务 A 提交之后的数据,而事务 A 回滚前的中间状态对 B 不可见。但读已提交存在另一个问题:不可重复读。即同一个事务内,两次读取同一条数据,结果可能不一样,因为中间有其他事务提交了修改。

不可重复读示例

| 时间 | 事务 A | 事务 B |
|------|---------|---------|
| T1 | SELECT 余额 FROM 账户 WHERE id = 1 → 200 | |
| T2 | | UPDATE 账户 SET 余额 = 300 WHERE id = 1 |
| T3 | | COMMIT |
| T4 | SELECT 余额 FROM 账户 WHERE id = 1 → 300 | |

事务 A 内两次相同的查询得到了不同的结果。这种不一致在某些业务中是不可接受的——比如对同一份数据做加减运算时,如果中间数据被改了,结果会出错。

实现方式:InnoDB 在读已提交级别下,每条 SQL 语句执行时都会生成一个新的 Read View。Read View 是 MVCC 中用于判断数据版本可见性的快照。每次读取都使用最新的 Read View,因此可以看到别的事务提交后的数据。

幻读问题:读已提交同样无法避免幻读。幻读指的是同一个事务内,两次范围查询的结果集行数不一样,因为有其他事务插入了符合条件的新行。由于 Read View 是语句级别的,新插入的行只要已提交,就会被后续查询看到。

实际使用:读已提交是很多数据库(如 Oracle、PostgreSQL)的默认隔离级别。它在避免脏读的同时,提供了比可重复读更高的并发性能。如果你的业务逻辑能容忍不可重复读和幻读(比如只用单行查询,且不依赖读取的稳定性),可以使用这一级别。但在 MySQL 中,默认级别是可重复读,更换默认级别需谨慎评估所有业务代码的影响。


6.2.3 可重复读(Repeatable Read)

定义:同一个事务内,多次读取同一行数据的结果始终一致,即使其他事务已经提交了修改。

这是 MySQL InnoDB 的默认隔离级别。它解决了不可重复读问题,保证事务内部读到的数据是稳定的。

不可重复读的解决:可重复读级别下,InnoDB 在事务第一次读取时生成一个 Read View,并且整个事务期间一直使用这个视图。因此,即使别的事务提交了修改,本事务仍然看到事务开始时的数据版本。

实现方式(MVCC + 锁)

  • 对于普通 SELECT,通过 MVCC 读取符合 Read View 的历史版本,不加锁。
  • 对于 SELECT ... FOR UPDATEUPDATEDELETE 等加锁语句,会按当前读(读取最新已提交版本)执行,并加锁防止其他事务修改。

幻读问题:可重复读级别下,普通读写操作不会出现幻读吗?在 InnoDB 中,对于快照读(普通 SELECT),确实不会出现幻读,因为 Read View 固定了可见性,新插入的行对本事务不可见。但对于当前读(加锁的 SELECT 或更新操作),如果只依靠 MVCC,仍可能发生幻读——比如事务 A 用 SELECT ... FOR UPDATE 锁定了一个范围,事务 B 却可以在该范围内插入新行。

InnoDB 解决当前读幻读的办法是 间隙锁(Gap Lock)。在可重复读隔离级别下,当事务使用范围条件加锁时,InnoDB 不仅锁定扫描到的行,还会锁定索引记录之间的间隙,阻止其他事务在间隙中插入数据。这种“记录锁 + 间隙锁”的组合称为临键锁(Next-Key Lock),它有效地在物理层面阻止了幻读的插入操作。

因此,InnoDB 在可重复读下几乎完全解决了幻读(至少对于加锁操作是这样的)。这也是为什么 MySQL 可以让可重复读成为默认级别,而很多其他数据库还需要升级到串行化才能防幻读。

实际使用:可重复读是 MySQL 最推荐的隔离级别,适用于绝大多数业务场景。它兼顾了数据一致性(无脏读、无不可重复读、几乎无幻读)和较高的并发性能(MVCC 无锁读)。开发时需要注意的点:

  • 避免长事务,因为 Read View 会一直保持,导致 Undo Log 无法清理,增加回滚段膨胀风险。
  • 如果业务强烈需要读到最新提交的数据(比如库存扣减后其他事务要立即看到),可以在单条查询中使用 SELECT ... FOR UPDATE 或调整为读已提交。

6.2.4 串行化(Serializable)

定义:事务按顺序一个接一个执行,完全隔离,如同单线程。

串行化是隔离性最强的级别,它从根本上消除了脏读、不可重复读、幻读的可能性,因为事务之间不再并发执行。

实现方式:InnoDB 在串行化级别下,对于所有普通 SELECT 语句,自动隐式转换为 SELECT ... FOR SHARE(即加共享锁)。这样,一个事务读取数据时,其他事务就不能修改这些数据,直到前者提交。此外,更新操作依然加排他锁。当读写锁冲突时,事务会被阻塞等待,从而强制顺序化。

需要注意的是,串行化虽然避免了并发问题,但也极大地降低了并发性能。如果大量事务同时读写同一张表,等待锁的时间会迅速增加,系统吞吐量暴跌。

适用场景:极其罕见。通常只用于对数据一致性有极度苛求且并发量极小的场景,比如银行的核心总账系统,或者包含复杂依赖关系的批量任务。对于绝大多数互联网应用,串行化都不是合理选择。


6.2.5 隔离级别的设置与查看

设置全局隔离级别(影响所有新连接):

SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

设置当前会话隔离级别(仅影响当前连接):

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

查看当前隔离级别

SELECT @@transaction_isolation;          -- MySQL 8.0
SELECT @@tx_isolation;                     -- MySQL 5.7

编程建议

  • 一般情况下,保持默认的可重复读即可。
  • 如果需要更高的并发写入性能,可以考虑将特定连接设置为读已提交(例如批处理任务),但必须确认业务逻辑能容忍不可重复读和幻读。
  • 绝对不要在代码中依赖读未提交的数据正确性。
  • 隔离级别改变会影响锁的行为,务必在测试环境充分验证。

小结:四种隔离级别是开发者和 DBA 在一致性与性能之间权衡的核心工具。理解每种级别的现象和原理,才能在遇到并发 bug 时准确诊断,并采取正确的隔离策略或改写 SQL。MySQL 的可重复读 + 间隙锁组合,已经在相当程度上平衡了可靠性与性能,这也是它被广泛采用的重要原因。