很多人学 MySQL 并发控制时,容易把事务、隔离级别、锁、MVCC 混在一起,背了一堆概念,真正遇到问题还是分不清。

这篇笔记的目标只有一个:

搞清楚 MySQL 到底是怎么在高并发下做到“尽量正确、尽量高效”的。

先记住一句总纲:

  • 写写冲突,主要靠锁

  • 读写并发,主要靠MVCC

  • 能看到什么数据,由隔离级别 + Read View决定

  • 事务能不能安全提交,靠 Undo Log + Redo Log


一、先说结论:MySQL 并发控制的核心思路

MySQL,准确地说是 InnoDB,并发控制不是靠单一机制完成的,而是几套机制共同配合:

机制主要解决什么问题关键词
事务保证一组操作要么都成功,要么都失败ACID
隔离级别规定事务之间“互相能看到多少”RU、RC、RR、SERIALIZABLE
锁解决写写冲突、当前读冲突行锁、间隙锁、临键锁
MVCC提高读写并发能力快照读、Read View、Undo Log
Undo Log / Redo Log保证回滚与崩溃恢复原子性、持久性

可以把它理解成一句话:

InnoDB 的并发控制,本质上是“锁负责互斥,MVCC 负责让读尽量不阻塞写”。


二、事务四大特性(ACID)

事务是并发控制的起点,因为没有事务,就谈不上隔离。

特性含义InnoDB 主要依赖什么保证
原子性(Atomicity)事务中的操作要么全部成功,要么全部回滚Undo Log
一致性(Consistency)事务执行前后,数据始终处于合法状态由原子性、隔离性、持久性和业务约束共同保证
隔离性(Isolation)多个事务并发执行时互不干扰锁 + MVCC
持久性(Durability)事务提交后数据不会因为宕机而丢失Redo Log

这里有一个很容易写错的点:

一致性不是某一个单独组件直接“保证”出来的,而是事务机制、约束条件、隔离控制共同作用的结果。


三、为什么会有并发问题

多个事务同时访问同一份数据时,如果没有隔离,就会出现“你改你的、我读我的,结果彼此影响”的情况。

最经典的三个并发问题是:

  • 脏读

  • 不可重复读

  • 幻读

1. 脏读

一个事务读到了另一个事务尚未提交的数据。如果对方随后回滚,那么这次读取到的就是“脏”的。

一句话理解:

读到了别人还没正式生效的数据,就是脏读。

2. 不可重复读

同一个事务中,两次读取同一行记录,结果却不一样。

原因通常是:两次读取之间,别的事务提交了对这行数据的修改。

一句话理解:

关注的是“同一行数据前后两次值变了”。

3. 幻读

同一个事务中,按照相同查询条件前后读取两次,发现结果集中的“行数”变了,好像凭空多出或少了几行。

典型原因是:另一个事务在这个范围内插入了新记录,或者删除了符合条件的记录。

这里要特别纠正一个常见误区:

幻读的标准定义,不是“当前读和快照读结果不同”,而是“同一事务按相同条件重复查询,结果集发生了变化”。

当前读和快照读结果不同,确实会让人感觉像“幻觉”,但那不是幻读的标准定义。


四、隔离级别是怎么限制并发问题的

隔离级别越高,一致性越强;但通常并发性能越低。

隔离级别脏读不可重复读幻读典型实现特点并发性能
READ UNCOMMITTED允许允许允许几乎不做读隔离最高
READ COMMITTED不允许允许允许每次快照读生成新的 Read View较高
REPEATABLE READ不允许不允许InnoDB 中对很多场景做了抑制快照读复用 Read View,当前读配合临键锁中等
SERIALIZABLE不允许不允许不允许读写强串行化,普通读也会转为加锁读最低

InnoDB 默认隔离级别是 REPEATABLE READ。

下面按级别分别理解。

1. READ UNCOMMITTED(读未提交)

  • 普通读可以读到其他事务未提交的数据

  • 会出现脏读、不可重复读、幻读

  • 隔离性最弱,但并发度最高

这个级别几乎不在生产中使用。

2. READ COMMITTED(读已提交)

  • 只能读到其他事务已经提交的数据

  • 每次执行快照读时,都会重新生成一个 Read View

  • 可以避免脏读

  • 但不能避免不可重复读,也不能避免幻读

这也是很多数据库的默认隔离级别,但 MySQL InnoDB 默认不是 RC,而是 RR。

3. REPEATABLE READ(可重复读)

  • 同一个事务内,多次快照读通常看到的是同一份一致性视图

  • 因为第一次一致性读生成的 Read View 会被后续快照读复用

  • 所以它能避免不可重复读

InnoDB 在这个级别下还做了一件非常关键的事:

  • 对于 当前读(SELECT ... FOR UPDATE、SELECT ... FOR SHARE、UPDATE、DELETE 等),会使用 Record Lock / Gap Lock / Next-Key Lock

  • 通过锁住“记录本身 + 记录之间的间隙”,减少范围内插入带来的幻读问题

所以一个更严谨的说法是:

在 InnoDB 的 RR 隔离级别下,快照读依靠 MVCC 保持一致视图,当前读依靠临键锁抑制幻读。

4. SERIALIZABLE(串行化)

  • 普通 SELECT 也会转成加锁读

  • 读写之间互相影响更强

  • 并发能力最差,但隔离性最强

生产环境中通常只在极少数强一致、低并发场景使用。


五、锁到底锁了什么

很多人一说行锁,就以为是“锁住一行”。这个说法不够准确。

更严谨地说:

InnoDB 的行级锁,本质上是加在索引记录上的。

这意味着:

  • 命中索引时,锁范围更精确

  • 没有合适索引时,可能扫描大量记录,锁范围也会变大

1. Record Lock(记录锁)

  • 锁住某条已经存在的索引记录

  • 主要用于防止别的事务修改这条记录

2. Gap Lock(间隙锁)

  • 锁住索引记录之间的“间隙”

  • 目的是防止别的事务在这个区间插入新记录

3. Next-Key Lock(临键锁)

  • 记录锁 + 间隙锁

  • 既锁住已有记录,也锁住记录前后的区间

  • 是 InnoDB 在 RR 下抑制幻读的重要手段

4. Intention Lock(意向锁)

  • 表级锁

  • 用来表示“事务准备在某些行上加什么类型的锁”

  • 目的是让表锁和行锁之间能更高效地协调

记忆方式:

  • 记录锁:锁已有数据

  • 间隙锁:锁插入空间

  • 临键锁:两者一起锁


六、MVCC 是什么,为什么它能提升并发

MVCC,全称 Multi-Version Concurrency Control,多版本并发控制。

它的核心思想不是“把锁做得更轻”,而是:

让读请求尽量去读“历史版本”,从而避免读写互相阻塞。

也就是说:

  • 写操作修改最新版本

  • 读操作可以根据自己的事务视图,读取合适的旧版本

这样大部分普通 SELECT 就不需要加锁。


七、MVCC 的两个关键角色:Undo Log 和 Read View

MVCC 能工作起来,离不开两个核心部件:

  • Undo Log:保存旧版本数据

  • Read View:决定当前事务“能看见哪个版本”

1. Undo Log:版本链从哪里来

当一行记录被修改时,InnoDB 不只是简单覆盖新值,还会把旧版本信息保存在 Undo Log 中。

这样一来,一条记录就可能形成一条“版本链”:

最新版本 -> 更早版本 -> 更更早版本

快照读时,如果最新版本对当前事务不可见,就顺着这条链往前找,直到找到一个可见版本。

2. 行记录中的隐藏字段

InnoDB 每行记录里会维护几个与 MVCC 相关的隐藏字段:

隐藏字段作用
DB_TRX_ID最近一次修改这条记录的事务 ID
DB_ROLL_PTR回滚指针,指向旧版本 Undo Log
DB_ROW_ID行 ID。只有在没有主键时,InnoDB 才会使用它作为内部标识

可以这样记:

  • DB_TRX_ID 负责说明“是谁改的”

  • DB_ROLL_PTR 负责说明“上一个版本在哪”


八、什么是快照读,什么是当前读

这两个概念特别重要,也是很多并发问题讲不清的根源。

1. 快照读

快照读读的不是“此刻数据库里最新提交的值”,而是:

当前事务根据 Read View 判断后,当前事务应该看到的那个版本。

典型场景:

  • 普通 SELECT

特点:

  • 一般不加锁

  • 读到的是一致性视图

  • 主要依赖 MVCC

2. 当前读

当前读读的是记录的最新版本,并且通常会加锁,防止并发修改。

典型场景:

  • SELECT ... FOR UPDATE

  • SELECT ... FOR SHARE

  • UPDATE

  • DELETE

特点:

  • 读取最新版本

  • 通常伴随加锁

  • 主要依赖锁机制

一句话区分:

快照读看“我该看到什么”,当前读看“现在最新是什么”。


九、Read View 到底长什么样

Read View 可以理解为:

某个时刻,InnoDB 为事务拍下的一张“活跃事务快照”。

它会记录当前有哪些事务还没提交,然后据此判断某个版本对当前事务是否可见。

Read View 的关键字段

字段名含义记忆方式
m_ids创建 Read View 时,系统中还活跃的读写事务 ID 列表活跃事务名单
m_up_limit_idm_ids 中最小的事务 ID低水位
m_low_limit_id当前系统下一个将要分配的事务 ID高水位
m_creator_trx_id创建这个 Read View 的事务 ID我自己的事务号

这里非常容易记反,必须纠正:

  • m_up_limit_id 才是低水位

  • m_low_limit_id 才是高水位

别被字段名表面含义带偏了。


十、可见性判断规则

假设我们正在判断某条记录的某个版本能不能被当前事务看到,主要看这个版本上的 DB_TRX_ID。

判断逻辑可以记成下面几步:

1. 如果 DB_TRX_ID == m_creator_trx_id

说明这是当前事务自己改的,可见。

2. 如果 DB_TRX_ID < m_up_limit_id

说明修改它的事务,在当前 Read View 创建前就已经提交了,可见。

3. 如果 DB_TRX_ID >= m_low_limit_id

说明修改它的事务,是在当前 Read View 创建之后才开始的,或者至少对当前视图来说太新了,不可见。

4. 如果 m_up_limit_id <= DB_TRX_ID < m_low_limit_id

这时候要去 m_ids 里查:

  • 如果能找到,说明这个事务在创建 Read View 时仍然活跃,不可见

  • 如果找不到,说明它在创建 Read View 前已经提交,可见

5. 如果当前版本不可见

就顺着 DB_ROLL_PTR 去 Undo Log 里找更老的版本,继续判断,直到找到可见版本为止。

一句话总结:

Read View 决定“哪些事务对我来说太新”,Undo Log 负责把旧版本找出来。


十一、RC 和 RR 下,Read View 有什么区别

这也是面试高频题。

READ COMMITTED

  • 每次执行快照读,都会创建新的 Read View

所以:

  • 事务中第一次 SELECT 和第二次 SELECT 看到的结果可能不同

  • 因为中间别人提交的数据,会进入新的视图

REPEATABLE READ

  • 事务中的第一次一致性读创建 Read View

  • 后续快照读复用这一份 Read View

所以:

  • 同一事务内多次快照读结果通常一致

  • 这就是它能避免不可重复读的关键原因

记忆口诀:

RC 是“每次读都重新拍照”,RR 是“第一次拍照后一直复用”。


十二、InnoDB 到底是怎么把锁和 MVCC 组合起来的

现在把前面的内容串起来,就容易了。

场景一:普通查询很多

如果大量请求只是普通 SELECT:

  • InnoDB 优先使用 MVCC

  • 读历史可见版本

  • 尽量不加锁

结果就是:

  • 读不容易阻塞写

  • 写也不容易阻塞普通读

场景二:要修改数据

如果事务要 UPDATE、DELETE,或者 SELECT ... FOR UPDATE:

  • InnoDB 进入当前读

  • 读取最新版本

  • 对目标记录或范围加锁

结果就是:

  • 写写冲突能被控制

  • 关键范围内的插入也能被约束

场景三:范围查询 + 防幻读

在 RR 下,如果是锁定范围查询:

  • InnoDB 会使用 Next-Key Lock

  • 不仅锁住已有记录,还锁住间隙

结果就是:

  • 别的事务不能随便往这个范围插入新数据

  • 从而抑制幻读

所以可以得出一个完整结论:

MySQL 并发控制不是“锁或 MVCC 二选一”,而是“普通读尽量走 MVCC,当前读和写操作交给锁”。


十三、几个最容易混淆的点

1. 幻读不是“当前读和快照读结果不同”

那只是现象相似,不是标准定义。

2. 可重复读不等于所有读结果永远完全一致

在 RR 下:

  • 快照读看到的是事务视图中的一致结果

  • 当前读看到的是最新版本

所以这两类读本来就可能不同。

3. 行锁本质上是索引锁

如果没有命中索引,锁的范围和代价都会变大。

4. MVCC 不是完全不用锁

MVCC 主要优化的是读写并发,不是替代所有锁。

写操作之间、当前读之间,依然主要靠锁来协调。


十四、面试怎么回答最顺

如果面试官问:“MySQL 的并发控制是怎么完成的?”

可以按下面这个顺序回答:

  1. 先说 InnoDB 不是只靠锁,而是 锁 + MVCC + 隔离级别 共同完成并发控制

  2. 再说锁主要解决写写冲突和当前读冲突,MVCC 主要提升普通读和写并发

  3. 再补充 MVCC 的底层依赖 Undo Log + Read View

  4. 说明 RC 和 RR 的差别,本质上是 Read View 生成时机不同

  5. 最后补一句 RR 下的当前读会用 Next-Key Lock 来抑制幻读

可以直接背的版本:

InnoDB 的并发控制本质上是锁和 MVCC 的组合。普通 SELECT 走 MVCC,通过 Undo Log 保存历史版本,通过 Read View 判断版本可见性,从而实现读不阻塞写。UPDATE、DELETE、SELECT ... FOR UPDATE 这类当前读或写操作,则通过记录锁、间隙锁、临键锁来控制并发冲突。RC 和 RR 的核心差别在于 Read View 的生成时机不同,RR 下还会通过 Next-Key Lock 抑制范围读带来的幻读问题。


Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐