致敬读者
博主相关
文章前言
MySQL(特别是 InnoDB 引擎)中事务与锁的核心概念 MVCC (多版本并发控制)。这是面试中关于数据库并发控制必考的高频知识点,理解它对掌握 MySQL 事务隔离级别、解决并发问题至关重要。
核心目标: MVCC 的核心目标是在保证一定事务隔离级别的前提下,大幅提高数据库的并发读性能。它通过允许读操作(SELECT
)在不加锁的情况下访问数据的历史版本来实现“读不阻塞写,写不阻塞读”(非完全互斥,后面会解释细节)。
在传统的基于锁的并发控制(如行锁)下:
SELECT ... FOR SHARE
或默认隔离级别下的某些读),另一个事务想写(UPDATE/DELETE
)同一行就需要等待读锁释放。UPDATE/DELETE/INSERT ... FOR UPDATE
),另一个事务想读同一行就需要等待写锁释放。这种互斥在高并发读场景下会导致严重的性能瓶颈。MVCC 就是为了优化“读-写”冲突而设计的。
MVCC 的核心思想是:为每一行数据维护多个版本(历史快照)。当一个事务开始时,它看到的是一个“快照”数据库,这个快照包含在该事务开始时刻已经提交的所有数据版本。事务内部的查询操作都基于这个快照进行,不受其他并发事务写入新版本的影响(直到它自己提交)。
InnoDB 实现 MVCC 依赖几个关键机制:
隐藏的系统列: 每行数据(InnoDB 的聚簇索引记录)都包含两个(有时三个)隐藏的系统列:
DB_TRX_ID
(6字节): 记录创建这条数据版本(或最后一次修改它)的事务ID。 当事务插入或更新一行时,会将自己的唯一事务ID(单调递增)写入该行的 DB_TRX_ID
。DB_ROLL_PTR
(7字节): 回滚指针。 指向该行数据在 Undo Log 中的上一个版本记录。这形成了一个单向链表,称为“版本链”。DB_ROW_ID
(6字节,可选): 行ID。 如果表没有定义主键,InnoDB 会自动生成一个隐藏的单调递增行ID作为聚簇索引。与 MVCC 直接关系不大,但影响物理存储。Undo Log (回滚日志):
UPDATE
或 DELETE
操作时:
UPDATE
: 先将当前行的数据(修改前)拷贝到 Undo Log 中,形成旧版本,然后修改当前行的数据(新版本),并将当前行的 DB_ROLL_PTR
指向刚写入 Undo Log 的旧版本记录。新版本的 DB_TRX_ID
设置为当前事务ID。DELETE
: 逻辑上标记删除(设置一个删除标志位),并将当前行的数据拷贝到 Undo Log 作为旧版本,DB_ROLL_PTR
指向它。新版本(标记删除的版本)的 DB_TRX_ID
为当前事务ID。DB_ROLL_PTR
指针串联起一行数据的所有历史版本。Read View (读视图):
SELECT
语句(或者显式启动事务如 START TRANSACTION WITH CONSISTENT SNAPSHOT
)时,InnoDB 会为该事务生成一个 Read View。m_ids
: 生成 Read View 时,系统活跃(未提交) 的事务ID列表。min_trx_id
: m_ids
中的最小值。max_trx_id
: 生成 Read View 时,系统应该分配给下一个新事务的ID(即当前最大事务ID + 1)。creator_trx_id
: 创建该 Read View 的事务自身的ID(对于只读事务,可能是0)。DB_TRX_ID
< min_trx_id
: 说明该版本是在当前 Read View 创建之前就已经提交的,可见。DB_TRX_ID
>= max_trx_id
: 说明该版本是当前 Read View 创建之后才开启的事务修改的,不可见。DB_TRX_ID
在 min_trx_id
和 max_trx_id
之间:
DB_TRX_ID
在 m_ids
列表中: 说明创建该版本的事务在生成 Read View 时还处于活跃状态(未提交),不可见。DB_TRX_ID
不在 m_ids
列表中: 说明创建该版本的事务在生成 Read View 时已经提交了,可见。SELECT
)。MVCC 主要在 READ COMMITTED (RC) 和 REPEATABLE READ (RR) 这两个隔离级别下发挥作用。SERIALIZABLE 隔离级别通常退化到基于锁的并发控制,不使用快照读。READ UNCOMMITTED 直接读取最新数据,不涉及版本控制。
READ COMMITTED (RC):
SELECT
语句执行前,都会重新生成一个 Read View。DB_TRX_ID
在活跃事务列表 m_ids
中)不会被看到。REPEATABLE READ (RR) - InnoDB 默认隔离级别:
SELECT
语句(或 START TRANSACTION WITH CONSISTENT SNAPSHOT
)时生成一个 Read View。该事务后续的所有普通 SELECT
语句(快照读) 都复用这个最初的 Read View。DB_TRX_ID
必然大于 max_trx_id
或在 m_ids
之外且大于 min_trx_id
)在快照读中是不可见的。但是! InnoDB 在 RR 级别下使用 Next-Key Locks 来防止其他事务在当前事务查询涉及的范围内插入新行,从而在当前读(SELECT ... FOR UPDATE/SHARE
, UPDATE
, DELETE
)层面也避免了幻读。所以,InnoDB 的 RR 级别通过 MVCC (快照读) + Next-Key Lock (当前读) 的组合,实际避免了幻读。快照读:
SELECT
语句(不加 FOR UPDATE/SHARE
)。当前读:
SELECT ... FOR UPDATE
/ SELECT ... FOR SHARE
/ LOCK IN SHARE MODE
UPDATE
/ DELETE
/ INSERT
语句(这些操作需要先定位到最新的、可见的、未被删除的行版本进行修改,这个定位过程本身就是一种当前读)。关键点: MVCC 主要优化的是快照读。当前读依然严重依赖各种锁机制(行锁、间隙锁、Next-Key Lock)来保证并发安全。
优点:
缺点:
UPDATE
/DELETE
需要写 Undo Log 来维护旧版本,增加了 I/O 和存储开销。什么是 MVCC?它解决了什么问题?
InnoDB 如何实现 MVCC?(关键组件)
DB_TRX_ID
(事务ID),DB_ROLL_PTR
(回滚指针,指向 Undo Log)。Read View 是如何判断一个数据版本是否可见的?
trx_id
):
trx_id
< min_trx_id
-> 可见(已提交)。trx_id
>= max_trx_id
-> 不可见(未来事务)。min_trx_id
<= trx_id
< max_trx_id
:
trx_id
在 m_ids
(活跃事务列表) 中 -> 不可见(未提交)。trx_id
不在 m_ids
中 -> 可见(已提交)。RC 和 RR 隔离级别下 MVCC 的主要区别是什么?
SELECT
都生成新的 Read View。因此能读到最新已提交的数据。可能导致不可重复读和幻读。SELECT
时生成一个 Read View,后续快照读都复用这个 View。因此在整个事务中看到的是一致的快照。解决了不可重复读,并通过快照读避免了幻读(新插入行不可见),结合 Next-Key Lock 在当前读层面也避免了幻读。什么是快照读和当前读?MVCC 主要针对哪种?
SELECT
,基于 MVCC/Read View 读历史版本(快照),非阻塞。MVCC 主要优化快照读。SELECT ... FOR UPDATE/SHARE
, UPDATE
, DELETE
, INSERT
。读取数据最新已提交版本并尝试加锁。需要加锁,用于保证数据在修改时的确定性。MVCC 对此优化有限。MVCC 能完全避免加锁吗?
INSERT
/UPDATE
/DELETE
) 和 当前读(SELECT ... FOR UPDATE/SHARE
) 依然需要加锁(行锁、间隙锁、Next-Key Lock)来处理写-写冲突和保证当前读的数据一致性。ALTER TABLE
)也需要表级锁。MVCC 的缺点是什么?
为什么说 InnoDB 的 RR 级别实际避免了幻读?
SELECT ... FOR UPDATE
或 UPDATE
/DELETE
时,不仅锁住符合条件的现有行,还会锁住行之间的间隙,防止其他事务在查询范围内插入新行,从而避免了当前读时的幻读。什么是 Undo Log?它在 MVCC 中起什么作用?
DB_ROLL_PTR
指向 Undo Log 中的旧版本记录。长事务对 MVCC 有什么影响?
聚簇索引记录 (行)
+-----------------------+
| Data |
| DB_TRX_ID: 100 | <--- 最新版本,由事务100修改
| DB_ROLL_PTR: ------->|------+
+-----------------------+ |
|
v
Undo Log Segment
+-----------------------+
| Data (旧值) |
| DB_TRX_ID: 90 | <--- 上一个版本,由事务90修改
| DB_ROLL_PTR: ------->|------> (可能指向更早版本)
+-----------------------+ |
|
事务120 (Read View: m_ids=[110,115], min_trx_id=105, max_trx_id=121) |
|
访问该行: |
- 最新版本 trx_id=100 < min_trx_id(105)? YES -> 可见! |
(事务100在事务120开始前已提交) |
|
如果事务120是RC, 下次读可能生成新Read View看到新版本。 |
如果事务120是RR, 永远看到这个trx_id=100的版本(如果它可见)。|
理解 MVCC 是掌握 MySQL 高并发原理的核心。务必结合 Read View 生成规则和可见性判断,并区分快照读和当前读在不同隔离级别的行为,以及 MVCC 与锁机制的协同工作方式。面试时能清晰阐述这些点,就能在并发控制相关问题中脱颖而出。
文末寄语