
“余弦:码哥,上一章说道 MySQL 的全局锁、表锁、意向锁、间隙锁(Gap Lock)、记录锁(Record Lock)、临键锁(Next-Key Lock)以及死锁的形成和优化。
我去面试的时候滔滔不绝,感觉胜利在握,可是面试官忽然到:“什么是 MVCC,MySQL 有了各种锁?为什么还要射界 MVCC?”
这是个好问题!
MVCC(Multi-Version Concurrency Control)中文叫做多版本并发控制协议,是 MySQL InnoDB 引擎用于控制数据并发访问的协议。
今天我就带你从 MVCC 基本原理说起,并且教你鬼狐一般隔离级别、版本连、Read View 的作用。
在 MVCC 出现之前,数据库主要依靠锁机制来解决并发冲突。但锁机制存在明显的瓶颈:
锁的代价:
-- 传统锁机制下的并发问题示例
-- 事务1
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1; -- 获取写锁
-- 事务2(被阻塞)
BEGIN;
SELECT balance FROM accounts WHERE user_id = 1; -- 等待读锁,直到事务1提交
试想一下,如果一个线程准备执行 UPDATE 一行数据,如果这时候阻塞住了所有的 SELECT 语句,那么这个性能你能接受吗?
“余弦:“那肯定不行,所以 MVCC 的核心思想是为每个数据项维护多个版本,读写操作可以并发进行而不相互阻塞?”
聪明,MVCC 的核心思想是为每一行数据维护多个版本(通常是两个),通过某个时间点的“快照”(Snapshot)来读取数据,从而避免加锁带来的性能损耗,实现非阻塞的读操作:
在深入理解 MVCC 之前,我们现在还需要进步一了解和 MVCC 紧密关联的概念,隔离级别。
MySQL 的事务隔离级别有以下四种:
读未提交(Read Uncommitted):
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;读已提交(Read Committed,简写 RC):
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;(RC)可重复读(Repeatable Read,简写 RR):
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;(RR)-- 事务A
BEGIN;
SELECT * FROM users WHERE age BETWEEN 20 AND 30 FOR UPDATE; -- 会锁住age=20到30这个区间,甚至包括两边的“间隙”
-- 此时事务B的插入会被阻塞
INSERT INTO users (name, age) VALUES ('新用户', 25); -- 阻塞,直到事务A提交
串行化(Serializable)是指事务对数据的读写都是串行化的。
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;“余弦:为什么要隔离?
在并发环境下,如果不进行任何隔离控制,并发事务会引发哪些问题?隔离级别本质上就是为了解决这些问题而存在的。
一个事务读到了另一个未提交事务修改的数据。如果另一个事务中途回滚,那么第一个事务读到的数据就是“脏”的、无效的。
示例:

一个事务内,两次读取同一个数据项,得到了不同的结果。重点在于另一个已提交事务对数据进行了修改(UPDATE)。
示例:

一个事务内,两次执行同一个查询,返回的结果集行数不同。重点在于另一个已提交事务对数据进行了增删(INSERT/DELETE),像产生了幻觉一样。
示例:

不可重复读 vs 幻读:
为了解决上述问题,SQL 标准定义了四种隔离级别,严格程度从低到高。级别越高,能解决的问题越多,但并发性能通常越低。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
读未提交 (Read Uncommitted) | ❌ 可能 | ❌ 可能 | ❌ 可能 |
读已提交 (Read Committed) | ✅ 避免 | ❌ 可能 | ❌ 可能 |
可重复读 (Repeatable Read) | ✅ 避免 | ✅ 避免 | ❌ 可能 |
串行化 (Serializable) | ✅ 避免 | ✅ 避免 | ✅ 避免 |
“注意: 在 MySQL 的 InnoDB 引擎中,通过 Next-Key Locking 技术,在可重复读(Repeatable Read) 隔离级别下就已经可以避免绝大部分的幻读现象。 这是 MySQL 对标准隔离级别的增强,也是其默认使用该级别的重要原因。
另外,还有两个概念你需要掌握:
在这里先介绍下我根据 10 年工作经验和面试经验亲手打造的面试专栏 -》《互联网大厂面试高手心法 58 讲》专栏,原价 89元, 现在已经涨价到 20元,满 400 人立马涨价,不要再错过。
我向官方申请了 20 张 8 折优惠券,前 20 名还能再优惠下单,不要错过!!!
8 折优惠码

MVCC 的实现依赖于三个核心组件:隐藏字段、Undo Log 版本链和ReadView。
让我们通过架构图来理解它们的关系:

为了实现 MVCC,InnoDB 引擎给每一行都加了三个额外的字段 trx_id 和 roll_ptr。
数据行的物理结构:
+------------+-------------+---------------+----------+-----------+
| 字段头信息 | DB_TRX_ID | DB_ROLL_PTR | 列1数据 | 列2数据 |
+------------+-------------+---------------+----------+-----------+
| 5字节 | 6字节 | 7字节 | 变长 | 变长 |
+------------+-------------+---------------+----------+-----------+
Undo Log 是 MVCC 的基石,它记录了数据变更的历史版本。每次数据修改时,InnoDB 都会在 Undo Log 中保存修改前的数据副本。
Undo Log 的类型:
版本链的构建过程:

“余弦:现在问题来了,假如这个时候我有一个新的事务 400,那么我该读取 trx_id 为几的数据呢?
这就涉及到了另外一个和 MVCC 紧密相关的概念:Read View。
Read View 你可以理解成是一种可见性规则。前面你已经知道了 undolog 里面存放着历史版本的数据,当事务内部要读取数据的时候,Read View 就被用来控制这个事务应该读取哪个版本的数据。
class ReadView {
private:
trx_id_t m_low_limit_id; // 高水位:大于等于此值的事务不可见
trx_id_t m_up_limit_id; // 低水位:小于此值的事务可见
trx_id_t m_creator_trx_id; // 创建该ReadView的事务ID
ids_t m_ids; // 活跃事务ID列表
trx_id_t m_low_limit_no; // 用于Purge的事务号
};
Read View 最关键的字段叫做 m_ids,它代表的是当前已经开始,但是还没有结束的事务的 ID,也叫做活跃事务 ID。
Read View 只用于已提交读和可重复读两个隔离级别,它用于这两个隔离级别的不同点就在于什么时候生成 Read View。
可见性判断是 MVCC 最精妙的部分,它遵循一套严格的算法规则:

def check_visibility(trx_id, read_view):
# 规则1: 当前事务修改的数据始终可见
if trx_id == read_view.creator_trx_id:
returnTrue
# 规则2: 事务ID小于低水位,说明在ReadView创建前已提交
if trx_id < read_view.up_limit_id:
returnTrue
# 规则3: 事务ID大于等于高水位,在ReadView之后开始
if trx_id >= read_view.low_limit_id:
returnFalse
# 规则4: 检查事务是否活跃
if trx_id in read_view.ids: # 活跃事务列表
returnFalse
else:
returnTrue# 已提交事务
# 对版本链进行判断
def find_visible_version(version_chain, read_view):
for version in version_chain:
if check_visibility(version.trx_id, read_view):
return version
returnNone# 没有可见版本
MVCC 在不同隔离级别下的行为有显著差异,这主要通过 ReadView 的生成时机来控制
特点: 每次 SELECT 都会生成新的 ReadView。
-- 事务1
BEGIN;
SELECTnameFROMusersWHEREid = 1; -- 生成ReadView1,看到事务100的版本
-- 事务2修改并提交
UPDATEusersSETname = 'Charlie'WHEREid = 1;
COMMIT;
-- 事务1再次查询
SELECTnameFROMusersWHEREid = 1; -- 生成ReadView2,看到事务200的新版本
只在第一次 SELECT 时生成 ReadView,后续查询复用。
-- 事务1
BEGIN;
SELECTnameFROMusersWHEREid = 1; -- 生成ReadView,看到事务100的版本
-- 事务2修改并提交
UPDATEusersSETname = 'Charlie'WHEREid = 1;
COMMIT;
-- 事务1再次查询(可重复读)
SELECTnameFROMusersWHEREid = 1; -- 使用相同的ReadView,仍看到事务100的版本

面试官在问 MVCC 的时候,都是直接问你这几个问题。
这时候你就要简明扼要地把原理解释清楚。按照基本定义、实现机制、隔离级别的逻辑顺序来回答。
“MVCC 是 MySQL InnoDB 引擎用于控制数据并发访问的协议。 MVCC 主要是借助于版本链来实现的。在 InnoDB 引擎里面,每一行都有两个额外的列,一个是 trx_id,代表的是修改这一行数据的事务 ID。 另外一个是 roll_ptr,代表的是回滚指针。 InnoDB 引擎通过回滚指针,将数据的不同版本串联在一起,也就是版本链。 这些串联起来的历史版本,被放到了 undolog 里面。 当某一个事务发起查询的时候,MVCC 会根据事务的隔离级别来生成不同的 Read View,从而控制事务查询最终得到的结果。
余弦:MySQL 的默认隔离级别是可重复读,实际上互联网的很多应用都调整过这个隔离级别,降低为已提交读。
可重复读比已提交读更加容易引起死锁的问题,比如说我们之前就出现过一个因为临键锁引发的死锁问题。
而且已提交读的性能要比可重复读更好。
所以综合之下,我就推动公司去调整隔离级别,将数据库的默认隔离级别降低为已提交读。
“在调整了事务级别之后,万一需要可重复读的特性了,你怎么办?”
首先你要分析使用可重复读的场景:
实际上这种场景真的存在吗?大多数可重复读的需求都是代码没写好!!
比如想要两次查询是同样的数据,那我们可以在第一次读取后的数据缓存。
此外,幻读一般不会认为是一个问题。事务提交往往意味着业务已经结束,所以读到一个已经提交的事务的数据,不会损害业务的正确性。
最后介绍我人生的第一本书《Redis 高手心法》本书基于 Redis 7.0 版本,将复杂的概念与实际案例相结合,以简洁、诙谐、幽默的方式揭示了Redis的精髓。
本书完美契合你对一个具体技术学习的期望: Redis 核心原理、关键细节、应用场景以及如何取舍......
从 Redis 的第一人称视角出发,拟人故事化方式和诙谐幽默的言语与各路“神仙”对话,配合 158 张图,由浅入深循序渐进的讲解 Redis 的数据结构实现原理、开发技巧、运维技术和高阶使用,让人轻松愉快地学习。
如下图所示,上市后得到了许多读者的较好口碑评价,而且上过京东榜单!其中还有一些业界大佬、公司 CTO 的推荐。

