MySQL-隔离级别

195 阅读7分钟

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 READNext-Key Lock(记录锁 + 前置间隙锁)生效,既锁行也锁间隙,阻止范围插入 ⇒ 无幻读。​
SERIALIZABLE所有读皆视为锁定读,行锁 + 间隙锁均生效,同样消除幻读。

锁定读因为加了S/X锁,S锁和X锁互斥,所以都避免了脏读和不可重复读。但没有gap lock导致还是存在幻读问题。 对于幻读问题,在可重复度隔离级别以及串行化级别,加上了临键锁(记录锁+前置间隙锁)来锁住数据行及前置间隙,完全避免幻读

三、MVCC 实现原理

MySQL 的 MVCC 是通过以下两个核心机制实现的:

1. Undo Log 版本链表
  • 每条数据记录都带有隐藏字段:

    • trx_id:最近一次修改该记录的事务 ID
    • roll_pointer:回滚指针,指向旧版本数据
  • 每次更新时,当前记录被复制为旧版本,形成一条链表;最新数据在链表头部。

2. ReadView 可见性判断
  • 每次执行快照读(SELECT)时生成 ReadView,决定哪些数据版本对当前事务可见。

  • 包含:

    • creator_trx_id:当前事务 ID
    • m_ids:活跃事务 ID 列表
    • min_trx_id:当前系统中最小活跃事务 ID
    • max_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存储引擎-第二版》