文章目录
- [第一步:理解 Undo Log (回滚日志) - "时光机与草稿纸"](#第一步:理解 Undo Log (回滚日志) - “时光机与草稿纸”)
- [第二步:理解 MVCC (多版本并发控制)](#第二步:理解 MVCC (多版本并发控制))
- [第三步:理解 Read View - "时空过滤器"](#第三步:理解 Read View - “时空过滤器”)
- 四者如何协作解决读一致性问题?
- 总结
- 面试回答示例 (选一个)
事务隔离有两种实现思路:
- 添加排他锁(参考:排他锁,共享锁相关简介)
- 使用
MVCC
(多版本并发控制,即本篇重点内容)
本文用最简洁清晰的方式串联 undo log
、MVCC(多版本并发控制)
和 Read View(读视图)
理解它们如何协作解决读一致性 问题(脏读、不可重复读)
核心目标: 让不同的事务能看到数据库在"不同时间点"的快照状态(重要)
一句话总结:MVCC 通过为每个读事务创建一个独立的"时间点快照"(Read View),并利用 Undo Log 提供的历史版本,确保该事务只看到在快照创建时已提交的数据,从而隔离了未提交的修改(脏读)和快照创建后发生的修改(可重复读)
第一步:理解 Undo Log (回滚日志) - "时光机与草稿纸"
- 是什么?
- 是逻辑日志 ,记录数据修改前的状态 或反向操作(INSERT 对应 DELETE,DELETE 对应 INSERT,UPDATE 对应反向 UPDATE)
- 存储在回滚段 (Rollback Segments) 中,由
rollback segment header
管理(包含history list
)
- 核心作用:
- 事务回滚: 如果事务失败或显式
ROLLBACK
,利用 undo log 把数据恢复到事务开始前的状态 - MVCC 的基础: 提供数据的历史版本! 这是解决读一致性的核心
- 事务回滚: 如果事务失败或显式
- 生命周期与持久性:
- 记录时机: 在数据被修改(INSERT/UPDATE/DELETE)之前,其旧版本信息/反向操作就被写入 undo log。
- 持久化: undo log 本身的修改也遵循 WAL 原则 !修改 undo log 的操作会先记录到 redo log 中,保证 undo log 的持久性没有这个,崩溃后无法回滚或提供历史版本
- 清理:
INSERT
操作的 undo log:事务提交后立刻可删(因为只有回滚用,对于 MVCC 来说,它不需要提供这个新行的"历史版本",因为它根本就没有历史)UPDATE
/DELETE
操作的 undo log:事务提交后不能立刻删 !它们被加入到回滚段的history list
由后台的purge
线程在确定没有任何事务(包括活跃事务和快照读事务)需要访问这个旧版本时,才安全删除这是 MVCC 能提供历史版本的关键
- 类比(延续书桌书架):
- 把 undo log 想象成你修改书页时使用的 "草稿纸" 或 "时光机按钮"
- 当你修改书桌上某页书时(UPDATE/DELETE):
- 你会先在草稿纸上记录下这页书原来的内容(记录旧版本到 undo log)
- 或者,你按下了"时光机按钮"标记了这一刻的状态(创建了一个历史版本点)
- 如果改错了想撤销(ROLLBACK),就按照草稿纸把书页恢复原状(利用 undo log 回滚)
- 即使你提交了修改(把书放回书架),草稿纸上旧版本的记录(undo log)也不会马上扔掉(加入 history list),因为可能还有其他人(其他事务)正在看你修改前的书页内容(快照读)
*快照读和当前读:
- 快照读: 简单的select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。
- RC:每次select,都生成一个快照读。
- RR:开启事务后第一个select语句才是快照读的地方。
- 当前读: 读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:select ... lock in share mode(共享锁),select ... for update、update、insert、delete(排他锁)都是一种当前读。
第二步:理解 MVCC (多版本并发控制)
- 是什么?
- 一种并发控制机制,核心思想是:为数据维护多个版本 读操作(通常是
SELECT
)默认不加锁,而是读取特定版本的数据,从而避免读写冲突,提高并发性能 - 解决了快照读(普通 SELECT)的读一致性问题(脏读、不可重复读、幻读)
- 一种并发控制机制,核心思想是:为数据维护多个版本 读操作(通常是
- MVCC 需要什么?
- MVCC 的核心是提供数据的历史版本 ,让其他事务的
Read View
能够回溯到过去的状态。 - 当一个事务执行快照读 (SELECT) 时,它可能需要读取一行数据的旧版本,这个旧版本必须来源于:
- 该行在过去某个时间点被提交的 UPDATE 结果。
- 或者该行在过去某个时间点被删除前的状态 (对于
DELETE
操作)。
- MVCC 的核心是提供数据的历史版本 ,让其他事务的
- 实现依赖的三个关键:
- 隐藏字段 (InnoDB 行结构):
DB_TRX_ID
:最后修改此行数据的事务ID谁改了我,我就记下谁的ID。DB_ROLL_PTR
:指向该行数据上一个历史版本在 undo log 中的位置指针,相当于一个"指向上一个草稿纸/时光机标记点"的链接
- Undo Log: 提供数据的历史版本链通过
DB_ROLL_PTR
指针可以沿着链条回溯找到旧数据 - Read View: 决定当前事务能看到哪个版本的数据相当于一个"时空过滤器"
- 隐藏字段 (InnoDB 行结构):
第三步:理解 Read View - "时空过滤器"
- 是什么?
- 在事务执行第一个快照读 (
SELECT
) 时创建(或在事务开始时立即创建,取决于隔离级别配置,可重复读 级别下是第一个 SELECT 时创建以保证可重复读) - 它定义了当前事务启动瞬间 ,数据库的一个一致性快照视图事务后续的所有快照读都基于这个视图
- 在事务执行第一个快照读 (
- 核心内容 (判断数据可见性的规则):
m_ids
:生成 Read View 时,系统中所有活跃(未提交)事务的事务ID列表min_trx_id
:m_ids
中最小的事务IDmax_trx_id
:生成 Read View 时,系统应该分配给下一个事务的ID(即当前最大事务ID + 1)creator_trx_id
:创建该 Read View 的事务自身的事务ID(如果是只读事务可能为0)
- 如何判断一行数据对当前事务是否可见? (基于该行数据的
DB_TRX_ID
)- 如果
DB_TRX_ID
<min_trx_id
:说明该版本是在 Read View 创建前已提交 的事务修改的 -> 可见 - 如果
DB_TRX_ID
>=max_trx_id
:说明该版本是在 Read View 创建后才开启 的事务修改的 -> 不可见 - 如果
min_trx_id
<=DB_TRX_ID
<max_trx_id
:- 如果
DB_TRX_ID
在m_ids
中:说明修改该版本的事务在 Read View 创建时还活跃(未提交) -> 不可见 - 如果
DB_TRX_ID
不在m_ids
中:说明修改该版本的事务在 Read View 创建时已提交 -> 可见
- 如果
- 如果
DB_TRX_ID
==creator_trx_id
:说明该版本是当前事务自己修改的 -> 可见
- 如果
- 如果不可见怎么办?
- 沿着该行数据的
DB_ROLL_PTR
指针,在 undo log 版本链中找到上一个历史版本 - 对找到的历史版本再次应用上述可见性规则 ,直到找到一个对当前 Read View 可见的版本或到达链头
- 沿着该行数据的
四者如何协作解决读一致性问题?
- 防止脏读 (Dirty Read):
- 事务A修改数据未提交 -> 该行
DB_TRX_ID
= A (活跃) - 事务B (Read Committed 或 Repeatable Read) 生成 Read View:
m_ids
包含 A -> A 修改的版本对 B 不可见- B 只能看到 A 修改前的版本(通过
DB_ROLL_PTR
找到 undo log 中的历史版本)
- 事务A修改数据未提交 -> 该行
- 防止不可重复读 (Non-Repeatable Read) - 可重复读 级别:
- 事务B在时间点 T1 生成 Read View (RV1),读取数据得到版本 V1。
- 事务A在 T2 提交了修改,数据变为版本 V2 (
DB_TRX_ID
= A) - 事务B在 T3 再次读取同一行 数据:
- 使用同一个 RV1。
- V2 的
DB_TRX_ID
(A) 可能小于max_trx_id
但 RV1 创建时 A 可能活跃 (在m_ids
中) -> V2 对 RV1 不可见 - 通过
DB_ROLL_PTR
找到历史版本 V1 (RV1 创建时已提交的版本) -> 返回 V1。
- 结果: B 在事务内两次读同一行数据结果一致 (V1)
总结
- Undo Log:
- 作用: 回滚事务 + 提供数据历史版本 (MVCC 基础)
- 关键点: 逻辑日志,记录反向操作/旧值;修改也写 redo 保证持久;UPDATE/DELETE 的 undo log 提交后不立即删 (purge 线程清理)
- MVCC:
- 目的: 实现无锁快照读,解决读一致性问题 (
READ COMMITTED
读已提交 /REPEATABLE READ
可重复读 级别) - 三要素:
- 隐藏字段:
DB_TRX_ID
(谁改的),DB_ROLL_PTR
(指向上个版本) - Undo Log: 存储历史版本,形成版本链
- Read View: 决定当前事务能看到哪个版本
- 隐藏字段:
- 目的: 实现无锁快照读,解决读一致性问题 (
- Read View:
- 何时创建: 事务第一个快照读 时 (可重复读) 或 每个语句开始时 (读已提交)
- 内容:
m_ids
(活跃事务ID),min_trx_id
,max_trx_id
,creator_trx_id
- 规则: 基于
DB_TRX_ID
和 Read View 内容判断数据版本是否可见,不可见则沿DB_ROLL_PTR
找历史版本
- 何时创建: 事务第一个快照读 时 (可重复读) 或 每个语句开始时 (读已提交)
- 如何解决读问题:
- 脏读: Read View 屏蔽未提交事务的修改 (看历史版本)
- 不可重复读 (RR): 同一个 Read View 保证看到同一个快照 (总是找到同一个历史版本)
- 幻读 (快照读): Read View 屏蔽快照后插入的新行
一句话总结 MVCC
和 Read View
,undolog
:
MVCC 和 Read View 通过为每个读事务创建一个独立的"时间点快照"(Read View),并利用 Undo Log 提供的历史版本,确保该事务只看到在快照创建时已提交的数据,从而隔离了未提交的修改(脏读)和快照创建后发生的修改(可重复读)。
面试回答示例 (选一个)
- 问:Undo Log 有什么用?
DB_ROLL_PTR
指向上一个历史版本在 Undo Log 中的位置;- Undo Log 存储了数据的历史版本,形成一条版本链;
- 事务在快照读时会生成一个
Read View
,根据 Read View 定义的规则(活跃事务列表、最小最大事务ID等)和当前行数据的DB_TRX_ID
,沿着DB_ROLL_PTR
找到对该 Read View 可见的版本这样每个事务就能读到它启动时的一个一致性快照
- 问:MVCC 是怎么实现的?
MVCC 主要通过三个机制实现:
- 数据行里的隐藏字段
DB_TRX_ID
记录最后修改它的事务ID,DB_ROLL_PTR
指向上一个历史版本在 UndoLog 中的位置;- Undo Log 存储了数据的历史版本,形成一条版本链;
- 事务在快照读时会生成一个 Read View,根据 Read View 定义的规则(活跃事务列表、最小最大事务ID等)和当前行数据的
DB_TRX_ID
,沿着DB_ROLL_PTR
找到对该 Read View 可见的版本这样每个事务就能读到它启动时的一个一致性快照