MySQL的事务隔离级别
# MySQL的事务隔离级别
MySQL将事务分为不同的隔离级别,主要是为了解决在多事务并发场景下可能出现的数据一致性问题。通过不同级别的事务隔离,可以防止出现脏读、不可重复读和幻读等问题。
版本说明
本文写于 2022-09,基于 MySQL 8.0 InnoDB。
四种隔离级别的语义与 InnoDB 默认的可重复读(RR)+ MVCC/间隙锁实现机制未变,经 2026-07 复核仍适用。
# 事务的四大特性(ACID)
原子性(Atomicity):事务是不可分割的工作单位,事务中的操作要么全部成功,要么全部失败回滚。
一致性(Consistency):事务执行前后,数据库必须保持一致性状态。例如:A和B账户共有5000元,不管他们之间如何转账,事务结束后两账户的总金额应该仍然是5000元。
隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
持久性(Durability):一个事务一旦提交,其对数据库的修改就是永久性的,即使系统发生故障也不会丢失。
# 并发事务可能出现的问题
# 1. 脏读(Dirty Read)
当一个事务读取到另一个事务未提交的数据时,就会发生脏读。
示例:
- 事务A读取了事务B未提交的数据(余额100修改为200)
- 事务B随后回滚(余额恢复为100)
- 事务A使用了错误的数据(认为余额是200)
# 2. 不可重复读(Non-Repeatable Read)
在同一事务内,多次读取同一数据却得到不同的结果。
示例:
- 事务A第一次读取某行数据
- 事务B修改该行数据并提交
- 事务A再次读取该行数据,发现数据已变化
# 3. 幻读(Phantom Read)
在同一事务内,多次查询某个范围的记录,发现记录数量不一致。
示例:
- 事务A查询工资大于1000的员工数量为10人
- 事务B插入一条工资为1500的记录并提交
- 事务A再次查询,发现工资大于1000的员工变为11人
# 不可重复读 vs 幻读的区别
- 不可重复读:针对同一行被别的事务改掉(UPDATE),两次读值不同
- 解决方案:MVCC 快照读,或对该行加锁(
FOR UPDATE)
- 解决方案:MVCC 快照读,或对该行加锁(
- 幻读:针对同一范围内多出/少了行(INSERT / DELETE),两次读行数不同
- 解决方案:间隙锁 / next-key lock(InnoDB 的做法),或提到 SERIALIZABLE
- ⚠️ 常见错误说法是「幻读靠表级锁解决」——InnoDB 从不为此加表锁,它锁的是索引记录之间的间隙,代价比表锁小得多
# MySQL的四种隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED(读未提交) | 可能 | 可能 | 可能 |
| READ COMMITTED(读已提交) | 不可能 | 可能 | 可能 |
| REPEATABLE READ(可重复读) | 不可能 | 不可能 | 可能 |
| SERIALIZABLE(串行化) | 不可能 | 不可能 | 不可能 |
⚠️ 这张表是 SQL 标准的定义,不是 InnoDB 的实际行为。标准允许 RR 出现幻读,但 InnoDB 在 RR 下已经把幻读堵住了——这是面试里最高频的追问点,只背这张表会当场答错。
MySQL 默认的隔离级别是 REPEATABLE READ(可重复读),InnoDB 在这一级用两套机制分别对付两种读:
| 读的类型 | 语句 | 靠什么防幻读 |
|---|---|---|
| 快照读 | 普通 SELECT | MVCC。事务第一条 SELECT 时生成 Read View 并在整个事务内复用,之后别人插入的行根本不在这个视图里,自然看不见 |
| 当前读 | SELECT ... FOR UPDATE / FOR SHARE、UPDATE、DELETE | next-key lock(行锁 + 间隙锁)。把扫描范围内的间隙一并锁住,别的事务插不进来 |
所以对 InnoDB 更准确的表是这样:
| 隔离级别 | 脏读 | 不可重复读 | 幻读(InnoDB 实际) |
|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 |
| READ COMMITTED | 不可能 | 可能 | 可能 |
| REPEATABLE READ | 不可能 | 不可能 | 基本不可能(见下面的例外) |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 |
唯一还能构造出幻读的口子:在同一个事务里混用快照读和当前读。快照读看的是事务开始时的视图,当前读看的是最新已提交数据,两者不一致:
-- Session A
BEGIN;
SELECT * FROM t WHERE id > 10; -- 快照读,返回 0 行
-- Session B
INSERT INTO t VALUES (11, 'x'); COMMIT;
-- Session A(同一事务内)
SELECT * FROM t WHERE id > 10; -- 快照读,仍然 0 行 ✅ 不幻读
SELECT * FROM t WHERE id > 10 FOR UPDATE; -- 当前读,返回 1 行 ← 这就是"幻读"
UPDATE t SET name='y' WHERE id > 10; -- 同理,影响 1 行
2
3
4
5
6
7
8
9
10
11
Session B 的 INSERT 之所以能成功,是因为 Session A 一开始只做了快照读、没有加任何锁。如果 A 第一条就用 FOR UPDATE,next-key lock 会把 id > 10 的间隙锁住,B 的 INSERT 会被阻塞,幻读也就不存在了。
一句话结论:InnoDB 的 RR 下,纯快照读不会幻读,纯当前读也不会幻读,只有两者混用才会。
# 隔离级别的实现原理
MySQL通过多版本并发控制(MVCC)和锁机制来实现事务隔离:
MVCC(Multi-Version Concurrency Control):
- 每次更新操作都会生成一条回滚日志(undo log)
- 不同时间点启动的事务会创建不同的read-view
- 通过回滚日志可以找到数据的历史版本
回滚日志(undo log)管理:
- 当没有事务需要用到某个回滚日志时,该日志会被删除
- 系统中不存在比该回滚日志更早的read-view时,日志即可删除
# 如何查看和设置隔离级别
# 查看当前隔离级别
方式1:
SHOW VARIABLES LIKE 'transaction_isolation';
方式2:
SELECT @@transaction_isolation;
# 设置隔离级别
- 通过SET命令:
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;
-- level可选值:
-- REPEATABLE READ
-- READ COMMITTED
-- READ UNCOMMITTED
-- SERIALIZABLE
2
3
4
5
6
7
- 通过启动参数:
--transaction-isolation=REPEATABLE-READ
不同作用域说明:
- GLOBAL:影响后续新的会话
- SESSION:影响当前会话的后续事务
- 无关键词:只影响下一个事务
# 验证:亲手跑一遍再下结论
开两个终端连同一个实例,确认 RR 下快照读到底会不会幻读。
Session A:
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT COUNT(*) FROM t WHERE id > 10;
2
3
预期输出:
+----------+
| COUNT(*) |
+----------+
| 0 |
+----------+
2
3
4
5
Session B(不要关掉 A 的事务):
INSERT INTO t VALUES (11, 'x');
COMMIT;
2
回到 Session A,仍在同一个事务里:
SELECT COUNT(*) FROM t WHERE id > 10; -- 快照读
SELECT COUNT(*) FROM t WHERE id > 10 FOR UPDATE; -- 当前读
2
预期输出(两条结果不同,这就是上文说的那个口子):
+----------+
| COUNT(*) |
+----------+
| 0 | ← 快照读,看不到 B 插的行
+----------+
+----------+
| COUNT(*) |
+----------+
| 1 | ← 当前读,看到了
+----------+
2
3
4
5
6
7
8
9
10
再确认一次隔离级别确实是 RR,别在 RC 下白测:
SELECT @@transaction_isolation;
# 坑与边界
- Read View 是在第一条快照读时创建的,不是
BEGIN时。BEGIN之后隔了很久才发第一条 SELECT,中间别人提交的数据你是看得到的。要在事务起点就固定视图,用START TRANSACTION WITH CONSISTENT SNAPSHOT。 - RR 下的长事务会拖住 undo 不能回收。因为老的 Read View 还需要历史版本,undo 表空间会一路涨,
information_schema.INNODB_TRX里看到跑了几小时的事务就要警觉——这是 RR 最常见的运维代价。 - RR 的间隙锁是死锁的主要来源。线上如果大量因间隙锁死锁,把隔离级别降到 RC 是常见解法(RC 没有间隙锁),但要先确认业务能接受不可重复读,且 RC 下 binlog 不能用 STATEMENT 格式(会导致主从数据不一致),必须是 ROW。
SET GLOBAL改隔离级别对已建立的连接无效,只影响之后新建的会话;连接池里的老连接会一直用旧值,改完记得让连接池重建连接。- 不是所有引擎都吃这一套:隔离级别与 MVCC 是 InnoDB 的实现,MyISAM 没有事务,隔离级别对它完全无意义。