很多人学 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 UPDATESELECT ... FOR SHAREUPDATEDELETE 等),会使用 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_id m_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

  • 读历史可见版本

  • 尽量不加锁

结果就是:

  • 读不容易阻塞写

  • 写也不容易阻塞普通读

场景二:要修改数据

如果事务要 UPDATEDELETE,或者 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 判断版本可见性,从而实现读不阻塞写。UPDATEDELETESELECT ... FOR UPDATE 这类当前读或写操作,则通过记录锁、间隙锁、临键锁来控制并发冲突。RC 和 RR 的核心差别在于 Read View 的生成时机不同,RR 下还会通过 Next-Key Lock 抑制范围读带来的幻读问题。


Logo

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

更多推荐