人人都会AI编程

6.2 事务隔离级别

更新时间:2026-07-11

多个事务并发执行时,如果没有合理的控制,就可能互相干扰,读到对方修改过程中的“半成品”数据,或者在同一事务内两次读到不同的结果。事务隔离级别就是数据库提供给用户的一个“旋钮”,用来在数据一致性并发性能之间做权衡。

6.2.1 三种常见的并发读问题

在理解隔离级别之前,需要先明确数据库到底要解决哪些并发问题。业界通常用三个经典现象来描述:

脏读(Dirty Read)

事务 A 修改了一条数据,但还没有提交。事务 B 此时读到了这条未提交的数据,然后根据它做了后续操作。之后事务 A 由于某种原因回滚,事务 B 之前读到的数据就成了无效的“脏数据”。举个例子:

-- 事务 A(将被回滚)
UPDATE account SET balance = balance - 100 WHERE id = 1;  -- 扣款 100,此时余额变为 900
-- 事务 B
SELECT balance FROM account WHERE id = 1;  -- 读到 900
-- 事务 A 回滚
ROLLBACK;  -- 此时真实余额还是 1000

事务 B 基于 900 做了判断,但实际数据是 1000,这就违反了数据一致性。脏读是最严重的问题,几乎没有生产环境可以容忍。

不可重复读(Non-Repeatable Read)

同一个事务内,前后两次读取同一条数据,结果不一样,因为中间有其他事务提交了修改。比如:

-- 事务 A 开始
START TRANSACTION;
SELECT balance FROM account WHERE id = 1;  -- 读到 1000
-- 此时事务 B 执行了:
UPDATE account SET balance = 900 WHERE id = 1;
COMMIT;
-- 事务 A 再次读取
SELECT balance FROM account WHERE id = 1;  -- 读到 900,和刚才读到的 1000 不一致

事务 A 本期望在一次事务内看到的数据是稳定的,但中途被其他已提交的事务干扰,破坏了其内部的读一致性。

幻读(Phantom Read)

不可重复读是同一行数据值变了,幻读则是“多出来或少了某几行”。事务 A 在同一个事务内,两次执行相同的范围查询(比如 WHERE amount > 100),结果集的行数不一样,因为其他事务插入了符合条件的新行并提交。例如:

-- 事务 A
START TRANSACTION;
SELECT count(*) FROM orders WHERE amount > 100;  -- 返回 10 行
-- 事务 B 插入一条新订单
INSERT INTO orders (id, amount) VALUES (101, 150);
COMMIT;
-- 事务 A 再次查询
SELECT count(*) FROM orders WHERE amount > 100;  -- 返回 11 行,仿佛出现了“幻觉”

6.2.2 四种隔离级别及其行为

SQL 标准定义了四种隔离级别,每种级别对上述问题的容忍度不同:

| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|-----------|------|------------|------|
| 读未提交(READ UNCOMMITTED) | 可能 | 可能 | 可能 |
| 读已提交(READ COMMITTED) | 不可能 | 可能 | 可能 |
| 可重复读(REPEATABLE READ) | 不可能 | 不可能 | 可能(InnoDB 中大大抑制) |
| 串行化(SERIALIZABLE) | 不可能 | 不可能 | 不可能 |

下面逐一展开。

读未提交(READ UNCOMMITTED)

这个级别几乎没有隔离。事务可以读到其他事务未提交的修改(脏读)。在生产环境基本没有使用的价值,因为连最基本的正确性都无法保障。个别场景下可以用于临时统计或只读报表,但通常也不推荐。

读已提交(READ COMMITTED)

事务只能读到其他事务已经提交的数据,脏读被解决了。这是一个比较“干净”的级别,也是很多数据库(如 Oracle)的默认级别。但是,由于其他事务提交后,新数据就会立刻对本事务可见,所以在同一个事务内两次读同一条记录可能结果不同,不可重复读仍然存在。

在 MySQL 中,读已提交级别的 MVCC 行为是:每一次查询都会生成一个新的 Read View,因此总能看到最新的已提交数据。

可重复读(REPEATABLE READ)

这是 MySQL InnoDB 的默认隔离级别。它保证在一个事务内,多次读取同一条记录的结果一致——即使其他事务对这些记录做了修改并提交,本事务看不到。不可重复读被解决了。

对于幻读,SQL 标准认为可重复读级别仍可能出现。但是 InnoDB 通过间隙锁(Gap Lock)机制,在很大程度上抑制了幻读。例如,当你在可重复读级别下执行 SELECT ... FOR UPDATEUPDATEDELETE 等加锁语句时,InnoDB 不仅锁住已存在的行(记录锁),还会锁住索引记录之间的间隙,防止其他事务插入符合条件的行。因此在加锁读取时,基本不会出现幻读。但如果只是普通的快照读 SELECT,仍然可能看到刚插入的新行?这一点需要注意:在可重复读级别下,由于 MVCC 使用同一个 Read View,其他事务提交的插入操作对本事务的快照不可见,所以普通的 SELECT 也不会产生幻读。只有在当前读(SELECT ... FOR UPDATE)且未加间隙锁的情况下才可能出现,但 InnoDB 的间隙锁正好处理了这种情况。所以实际使用时,InnoDB 的可重复读已经能应付绝大多数场景下的幻读问题,不需要为了防幻读而强制升到串行化。

串行化(SERIALIZABLE)

最强的隔离级别。事务像排队一样一个接一个执行,完全串行化。所有读操作都会隐式加上读锁(共享锁),写操作加写锁(排他锁)。并发度最低、性能最差,但数据最安全。极少在生产中使用,通常只有像银行核心账务系统这类对一致性要求苛刻到极致的场景才会考虑。

6.2.3 查看与设置隔离级别

你可以通过以下命令查看当前会话和全局的隔离级别:

-- 查看当前会话级别
SELECT @@transaction_isolation;  -- MySQL 8.0
-- 或
SELECT @@tx_isolation;           -- MySQL 5.7

-- 查看全局级别
SELECT @@global.transaction_isolation;

修改隔离级别的语法为:

-- 仅对当前会话生效
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 对全局生效(新连接才会使用,不影响现有连接)
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

也可以在配置文件 my.cnf 中通过 transaction-isolation 参数指定默认级别。

6.2.4 如何选择隔离级别

在实际生产环境中,绝大多数 MySQL 应用都直接使用默认的 REPEATABLE READ。它解决了脏读和不可重复读,依靠间隙锁在加锁场景下避免了幻读,同时保持不错的并发性能。早期很多基于 MySQL 的架构都是围绕这个级别设计的,贸然调整为 READ COMMITTED 可能导致业务逻辑出错(例如依赖可重复读的 binlog 格式必须是 ROW,且要注意间隙锁失效带来的新问题)。

如果你明确知道自己的业务没有不可重复读的需求,且希望更高并发、减少间隙锁带来的锁冲突,可以将级别设为 READ COMMITTED。这个级别在互联网行业中的使用率也在增长,尤其是在使用了 binlog 格式 ROW 且主从复制场景下,READ COMMITTED 配合行级锁往往锁冲突更少,更适合高并发 OLTP。

READ UNCOMMITTEDSERIALIZABLE 则是两个极端,基本不在生产的主业务链路上使用。READ UNCOMMITTED 对一致性要求为零,SERIALIZABLE 性能损耗太大。只有极少数非关键场景(如内部临时数据分析、某些金融合规要求)才可能触及。

作为开发者,最重要的一点是:隔离级别不是一个可以随意切换的开关,它深刻影响 SQL 的加锁行为和可见性。在切换之前,务必进行充分测试,并确认应用层的事务逻辑不会因此产生错误。如果你接手一个已有系统,先执行 SELECT @@transaction_isolation 确认当前级别,再决定后续的开发与优化策略。