人人都会AI编程

15.3 事务隔离级别设置与验证

更新时间:2026-07-11

理解事务隔离级别不能只局限于理论。在实际开发中,你需要知道如何设置级别、如何确认当前生效的级别,以及如何通过具体的 SQL 操作来验证不同级别的行为差异。这一节将聚焦实操,帮助你真正“看见”隔离级别的作用。

15.3.1 查看当前隔离级别

MySQL 提供了两种查看事务隔离级别的方式:

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

-- 查看当前会话的隔离级别
SELECT @@session.transaction_isolation;

-- 或者使用 SHOW 语句
SHOW VARIABLES LIKE 'transaction_isolation';

从 MySQL 8.0 开始,系统变量名统一为 transaction_isolation(取代了早期版本中的 tx_isolation)。在 5.7 中两者均可用,但建议逐步迁移到新名称。

15.3.2 设置隔离级别

你可以根据需求在不同粒度上设置隔离级别:全局级别、会话级别,或者仅针对下一个事务。

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

SET GLOBAL transaction_isolation = 'READ-COMMITTED';

这会改变之后所有新连接的默认隔离级别,但已存在的连接不受影响。通常修改全局级别后需要重启应用连接池才能全部生效。需要有 SUPERSYSTEM_VARIABLES_ADMIN 权限。

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

SET SESSION transaction_isolation = 'REPEATABLE-READ';

此设置立即对当前会话生效,断开连接后失效。常用于临时调试或测试不同隔离级别下的行为。

3. 设置下一个事务的隔离级别(仅影响本事务)

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;
-- 该事务以 SERIALIZABLE 运行
COMMIT;

这种方式只改变紧接着的下一个事务的隔离级别,事务结束后自动恢复为会话默认值。对于需要临时使用特殊隔离级别的事务非常有用。

在生产环境中,最常用的隔离级别是 REPEATABLE-READ。如果要改为 READ-COMMITTED,请务必仔细评估业务影响,因为 InnoDB 在这两个级别下的加锁行为有显著差异——READ-COMMITTED 下没有间隙锁,可能导致幻读,但能减少死锁概率。

15.3.3 验证不同隔离级别的行为

理论上的脏读、不可重复读、幻读,只有亲手复现才能留下深刻印象。以下实验建议在测试库中操作,开启两个会话(Session A 和 Session B)模拟并发。

准备工作:

CREATE TABLE accounts (
    id INT PRIMARY KEY,
    balance DECIMAL(10,2)
) ENGINE=InnoDB;

INSERT INTO accounts VALUES (1, 100.00), (2, 200.00);
1. 脏读验证(Read Uncommitted)

脏读指一个事务读取到了另一个事务尚未提交的修改。

| 步骤 | Session A(READ-UNCOMMITTED) | Session B |
|------|--------------------------------------------------|--------------------------------------------|
| 1 | SET SESSION transaction_isolation = 'READ-UNCOMMITTED'; | |
| 2 | START TRANSACTION; | START TRANSACTION; |
| 3 | | UPDATE accounts SET balance = 150 WHERE id=1; |
| 4 | SELECT * FROM accounts WHERE id=1; -- 看到 150(脏读) | |
| 5 | | ROLLBACK; |
| 6 | SELECT * FROM accounts WHERE id=1; -- 变回 100 | |

在 Session A 的步骤 4 中,它读到了 Session B 未提交的修改(150),这就是脏读。当 B 回滚后,A 又读到原值 100,数据前后不一致。在 READ-COMMITTED 及以上级别中,A 在步骤 4 只会看到 100。

2. 不可重复读验证(Read Committed)

不可重复读是指同一个事务内,两次读取同一行数据,结果不一样(因为另一个事务提交了修改)。

| 步骤 | Session A(READ-COMMITTED) | Session B |
|------|-------------------------------------------|--------------------------------------------|
| 1 | SET SESSION transaction_isolation = 'READ-COMMITTED'; | |
| 2 | START TRANSACTION; | START TRANSACTION; |
| 3 | SELECT balance FROM accounts WHERE id=1; -- 得到 100 | |
| 4 | | UPDATE accounts SET balance = 200 WHERE id=1; |
| 5 | | COMMIT; |
| 6 | SELECT balance FROM accounts WHERE id=1; -- 得到 200(不可重复读) | |

READ-COMMITTED 下,每次查询都会获取最新已提交的数据快照,所以 A 在步骤 6 看到了 200,与步骤 3 不同。在 REPEATABLE-READ 级别下,A 在步骤 6 依然会得到 100,因为事务使用第一次查询时建立的快照。

3. 幻读验证(Repeatable Read 是否彻底解决?)

幻读指同一个事务内,以相同的条件查询,结果集的行数发生了变化(其他事务插入了新数据)。

| 步骤 | Session A(REPEATABLE-READ) | Session B |
|------|------------------------------------------|--------------------------------------------|
| 1 | SET SESSION transaction_isolation = 'REPEATABLE-READ'; | |
| 2 | START TRANSACTION; | START TRANSACTION; |
| 3 | SELECT * FROM accounts WHERE balance>0; -- 2 行 | |
| 4 | | INSERT INTO accounts VALUES(3, 300); |
| 5 | | COMMIT; |
| 6 | SELECT * FROM accounts WHERE balance>0; -- 还是 2 行(没有幻读) | |

REPEATABLE-READ 下,A 看到的结果都是基于事务开始时的快照,所以 B 插入的新行(id=3)对 A 不可见,似乎解决了幻读。但这主要得益于快照读。如果 A 执行当前读(如 SELECT ... FOR UPDATE),情况会变化:

| 步骤 | Session A | Session B |
|------|------------------------------------------|--------------------------------------------|
| 1 | START TRANSACTION; | |
| 2 | | INSERT INTO accounts VALUES(4, 400); |
| 3 | | COMMIT; |
| 4 | SELECT * FROM accounts WHERE balance>0 FOR UPDATE; -- 看到 4 行,包括 id=4(发生幻读) | |

因为 FOR UPDATE 是当前读,它会读取最新提交的数据,并且会加临键锁试图防止幻读。但如果 B 在 A 加锁之前就插入并提交,A 仍然会看到多出的行。所以在严格意义上,REPEATABLE-READ 并未完全杜绝幻读,只是通过快照读大幅减少了幻读发生的机会。要想彻底避免幻读,需使用 SERIALIZABLE

4. 串行化验证(SERIALIZABLE)

串行化通过将读操作隐式转为 SELECT ... FOR SHARE(8.0 之前是 LOCK IN SHARE MODE)来实现读写互斥,从而彻底避免并发异常。

| 步骤 | Session A(SERIALIZABLE) | Session B |
|------|------------------------------------------|--------------------------------------------|
| 1 | SET SESSION transaction_isolation = 'SERIALIZABLE'; | |
| 2 | START TRANSACTION; | START TRANSACTION; |
| 3 | SELECT * FROM accounts WHERE id=1; | |
| 4 | | UPDATE accounts SET balance=500 WHERE id=1; -- 等待锁 |
| 5 | COMMIT; | |
| 6 | | 步骤 4 的 UPDATE 获得锁,执行成功 |

SERIALIZABLE 下,A 的普通 SELECT 会在行上施加共享锁(且可能还有间隙锁),B 必须等待 A 提交后才能修改。这种级别并发度最低,只在极强一致性要求的场景中使用。

15.3.4 实际开发建议

  • 在线事务系统(如订单、支付):使用 REPEATABLE-READ,这是 InnoDB 的默认级别,能平衡一致性与并发性能。如果业务允许不可重复读且死锁严重,可考虑降为 READ-COMMITTED 并关闭间隙锁。
  • 报表或只读查询:通常不需要最高的一致性,继承会话级别即可。
  • 临时强一致性需求:可对单独事务使用 SERIALIZABLE,用完即恢复,避免全局影响。

隔离级别没有最佳,只有最合适。验证不同级别的行为不仅能加深对事务原理的理解,还能帮助你在排查数据异常时快速定位是否是隔离级别或加锁导致的问题。