MySQL 并发控制到底靠什么完成?把事务、锁和 MVCC 一次讲透就看这一篇!
很多人学 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_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
-
读历史可见版本
-
尽量不加锁
结果就是:
-
读不容易阻塞写
-
写也不容易阻塞普通读
场景二:要修改数据
如果事务要 UPDATE、DELETE,或者 SELECT ... FOR UPDATE:
-
InnoDB 进入当前读
-
读取最新版本
-
对目标记录或范围加锁
结果就是:
-
写写冲突能被控制
-
关键范围内的插入也能被约束
场景三:范围查询 + 防幻读
在 RR 下,如果是锁定范围查询:
-
InnoDB 会使用 Next-Key Lock
-
不仅锁住已有记录,还锁住间隙
结果就是:
-
别的事务不能随便往这个范围插入新数据
-
从而抑制幻读
所以可以得出一个完整结论:
MySQL 并发控制不是“锁或 MVCC 二选一”,而是“普通读尽量走 MVCC,当前读和写操作交给锁”。
十三、几个最容易混淆的点
1. 幻读不是“当前读和快照读结果不同”
那只是现象相似,不是标准定义。
2. 可重复读不等于所有读结果永远完全一致
在 RR 下:
-
快照读看到的是事务视图中的一致结果
-
当前读看到的是最新版本
所以这两类读本来就可能不同。
3. 行锁本质上是索引锁
如果没有命中索引,锁的范围和代价都会变大。
4. MVCC 不是完全不用锁
MVCC 主要优化的是读写并发,不是替代所有锁。
写操作之间、当前读之间,依然主要靠锁来协调。
十四、面试怎么回答最顺
如果面试官问:“MySQL 的并发控制是怎么完成的?”
可以按下面这个顺序回答:
-
先说 InnoDB 不是只靠锁,而是 锁 + MVCC + 隔离级别 共同完成并发控制
-
再说锁主要解决写写冲突和当前读冲突,MVCC 主要提升普通读和写并发
-
再补充 MVCC 的底层依赖 Undo Log + Read View
-
说明 RC 和 RR 的差别,本质上是 Read View 生成时机不同
-
最后补一句 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 抑制范围读带来的幻读问题。
更多推荐




所有评论(0)