MySQL事务隔离级别详解
在关系型数据库中,为了保证数据一致性和正确性,引入了事务(Transaction)机制。事务具有四大特性(ACID):原子性、一致性、隔离性、持久性。其中隔离性是事务与事务之间并发执行时的关键保证。
MySQL 作为最常用的数据库之一,支持 SQL 标准定义的四种隔离级别,并通过 MVCC + 锁机制 实现高效的并发控制。
一、四种隔离级别存在的问题(普通读)
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | ✅ | ✅ | ✅ |
| READ COMMITTED | ❌ | ✅ | ✅ |
| REPEATABLE READ | ❌ | ❌ | ✅(标准) ❌(InnoDB) |
| SERIALIZABLE | ❌ | ❌ | ❌ |
假设存在并发事务AB, A查数据,B修改数据,AB同时操作一条数据
RU级别: 由于select无间隙锁,无行锁,无快照保护, 读取的是最新数据
- A会读到B还未提交的数据(脏读)
- A多次读取同一数据的中途如果B修改了数据,所以数据两次读取不一致(不可重复读)
- A事务进行范围查找,B在范围中插入新数据会导致事务A产生数据幻影(幻读)
RC级别 由于RC级别开启了语句级别的快照,所以可以避免脏读,但无法解决不可重复读和幻读
- RC使用了MVCC机制保证是快照读,A不会读到未提交事务B的数据(B的事务ID存在于A readview中的活跃事务ID集合,B的修改对于A不可见)(解决脏读)
- 但仍然存在不可重复读的问题,因为每次事务A select都会生成新的readview,如果中途有事务B修改数据会导致数据行不可重复读;
- 也存在幻读的问题: mvcc下的readview频繁更新(每次读取基于不同快照),所以每次select会存在数据幻影。
RR级别
由于RR级别开启了事务级别的快照,所以可以处理脏读、不可重复读和幻读问题
-
脏读:innodb MVCC机制的快照读,不会读取其他事务修改但未提交的数据(其他事务在当前read view中属于当前活跃的事务,不可见)
-
不可重复读:与RC不同,只会开启事务级别的readview,readview在事务未结束前不会有任何修改,所以同一行数据可重复读
-
幻读:MVCC解决普通读下的幻读问题,事务A进行范围查找,会将范围中所有命中的记录的row_trx_id都和当前的事务read view做可见性判断,来避免幻读
串行化/序列化级别
MVCC快照读被禁用,普通读都认定为锁定读,读加s锁和间隙锁,彻底解决脏读、幻读、不可重复读等问题
- 脏读|不可重复读:加行锁S锁阻塞其他事务修改数据(X锁与S锁互斥,必须等待S锁释放),所以不会读到脏数据。其他事务不会影响到当前事务多次读取数据也是一致的,可重复读。
- 幻读:给范围中命中的索引记录加上行锁,给命中的索引前置间隙加上间隙锁,其他事务必须等待临键锁(行锁+间隙锁)释放以后才可以在范围内插入新数据或者修改数据,所以不存在数据幻影。
二、四种隔离级别存在的问题(锁定读)
select for update select lock in share mode update delete
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
|---|---|---|---|---|
| READ UNCOMMITTED | ❌ | ❌ | ✅ | 锁定读对行加 X/S 锁,阻塞未提交改动,消除脏读和不可重复读;但 gap-lock 被禁用,允许范围内插入 ⇒ 幻读。 |
| READ COMMITTED | ❌ | ❌ | ✅ | 只加X/S锁,不加 gap-lock ⇒ 幻读依旧可能(新行可插入)。 |
| REPEATABLE READ | ❌ | ❌ | ❌ | Next-Key Lock(记录锁 + 前置间隙锁)生效,既锁行也锁间隙,阻止范围插入 ⇒ 无幻读。 |
| SERIALIZABLE | ❌ | ❌ | ❌ | 所有读皆视为锁定读,行锁 + 间隙锁均生效,同样消除幻读。 |
锁定读因为加了S/X锁,S锁和X锁互斥,所以都避免了脏读和不可重复读。但没有gap lock导致还是存在幻读问题。 对于幻读问题,在可重复度隔离级别以及串行化级别,加上了临键锁(记录锁+前置间隙锁)来锁住数据行及前置间隙,完全避免幻读
三、MVCC 实现原理
MySQL 的 MVCC 是通过以下两个核心机制实现的:
1. Undo Log 版本链表
-
每条数据记录都带有隐藏字段:
trx_id:最近一次修改该记录的事务 IDroll_pointer:回滚指针,指向旧版本数据
-
每次更新时,当前记录被复制为旧版本,形成一条链表;最新数据在链表头部。
2. ReadView 可见性判断
-
每次执行快照读(SELECT)时生成
ReadView,决定哪些数据版本对当前事务可见。 -
包含:
creator_trx_id:当前事务 IDm_ids:活跃事务 ID 列表min_trx_id:当前系统中最小活跃事务 IDmax_trx_id:系统中下一个将要分配的事务 ID
3. 可见性判断规则
- 如果
trx_id == creator_trx_id:✅ 可见(自己写的) - 如果
trx_id < min_trx_id:✅ 可见(老版本,已提交) - 如果
trx_id ∈ m_ids:❌ 不可见(未提交活跃事务) - 如果
trx_id ≥ max_trx_id:❌ 不可见(未来事务)
若当前节点不可见,则从 undo 链表向前追溯,直到找到可见版本。
四、总结
-
默认隔离级别:REPEATABLE READ
- 结合 MVCC(多版本快照读) 与 Next-Key Lock(记录锁+间隙锁) ,既保证读一致性,又防止幻读。是并发性能和数据一致性的权衡。
- 普通读走事务级快照,不阻塞写;锁定读走 Next-Key Lock,阻塞冲突写入。
-
隔离级别对比
隔离级别 脏读 不可重复读 幻读 并发性能 READ UNCOMMITTED ✅ ✅ ✅ 最高(最弱隔离) READ COMMITTED ❌ ✅ ✅ 高 REPEATABLE READ ❌ ❌ ❌ 中 SERIALIZABLE ❌ ❌ ❌ 最低(最强隔离) -
何时使用何种隔离级别
- 高吞吐、可容忍幻读:READ COMMITTED + 必要时显式锁定读(FOR UPDATE)。
- 大多数 OLTP 业务:REPEATABLE READ(默认),无需额外配置。
- 绝对一致性、零幻读:SERIALIZABLE,但要付出并发性能代价。
-
索引与锁的配合
- 间隙锁(Gap-Lock)只能加在索引上;非索引列的范围操作会退化为全表扫描并锁住聚簇索引,效果等同于锁住全表。
- 设计表结构时,应根据查询条件建立合适索引,既能加锁精确范围,也能提升查询性能。
-
MVCC vs. 锁机制
- MVCC:解决“读哪个版本”的问题,不阻塞写。
- 锁(Record-Lock/Gap-Lock) :解决“谁能写入/插入”的问题,确保范围写入安全。
- 两者协同,实现高性能与高一致性的平衡。
核心理念:通过 MVCC 提升读并发,通过精细化的行锁和间隙锁防止并发写冲突,选择最合适的隔离级别,权衡性能与一致性。
RR级别是否能完全避免幻读?
会有一种的情况会导致RR级别下普通读也会产生幻读: 事务内将普通读和锁定读混合在一起。 情景1和2不符合业务逻辑,但确实存在幻读现象,解决办法是不要普通读和锁定读不要混用。 若业务确实需要“先查看再修改”,应一开始就用锁定读。
情景1:事务A先普通读再锁定读
事务A select * from T where t.id > 100; --返回id=300,id=400的数据
事务B insert into T (id,...) values (200,...)-- 插入并提交
事务A select * from T where t.id > 100 for update--返回id=200,300,400 出现幻读
情景2:事务A先普通读在更新(锁定读)
事务A select * from T where t.id = 100; --无数据
事务B insert into T (id,...) values (100,...) -- 插入数据并提交
事务A update T set T.xxx="xxx" where id=100-- 可以修改id=100的数据
参考
《高性能MySQL-第四版》
《极客时间-MySQL45讲》
《MySQL技术内幕-InnoDB存储引擎-第二版》