Theory · MySQL · InnoDB

MVCC:多版本并发控制

隐藏列 + undo 版本链 + ReadView —— 快照读不加锁,让读不阻塞写、写不阻塞读

一致性非锁定读

普通 SELECT 沿版本链挑一个"对自己可见"的历史版本,不持任何锁(consistent nonlocking reads)

ReadView 可见性判定

四步判定 + 沿 DB_ROLL_PTR 逐版回溯;源码 read0types.h 的 changes_visible() 逐行对齐

RR / RC 分野

实现完全相同,只差 ReadView 建立时机:RR 首次快照读建、全程复用;RC 每读新建

MVCC 是 MySQL 面试的事务必考题,和索引、锁并列三大件。这份 deck 的组织逻辑:先建立"为什么"(读写不阻塞),再拆三大件(隐藏列、undo 链、ReadView),把可见性算法落到源码,然后讲 RC/RR 差异、幻读、purge、二级索引这些追问链。所有结论都对照官方手册和 InnoDB 源码验证过,不是背的二手结论。

Why MVCC

没有版本机制,一致性读就得拿锁排队

只有锁的世界(悲观方案)

事务 A 在改第 5 行(持 X 锁),事务 B 想"只是读一下"也得等 A 提交才能拿 S 锁。读多写少的业务里,大量读请求被少量写阻塞,吞吐崩塌——隔离性靠"排队"硬扛。

多版本方案(MVCC)

改数据时把旧版本存进 undo log;读事务按自己的"快照视图"挑一个对自己可见的历史版本返回。普通 SELECT 全程无锁——手册称 consistent nonlocking reads(一致性非锁定读)。

维度效果
读 vs 写互不阻塞:读读旧版本,写写新版本,各行其是——这是 MVCC 的核心收益
写 vs 写仍靠行锁(X 锁)串行化,MVCC 不解决写写冲突
作用范围InnoDB 行级多版本;只服务 RC / RR 下的快照读(RU 是脏读、SERIALIZABLE 走锁,见后文)
一句话定位:MVCC = 写时留旧版本 + 读时按快照挑可见版本。它把"隔离性"的代价从读侧挪到了写侧的 undo 存储,换来读写并行。
讲 MVCC 先讲它解决什么问题:没有版本机制时,一致性读也要加共享锁,读写互斥。MVCC 的思路是把数据做成多版本,写的事务在写新版本、留旧版本,读的事务按快照挑一个对自己可见的旧版本,全程不加锁。注意边界:它只解决读和写的冲突,写和写还是靠锁;而且只服务 RC 和 RR 两个隔离级别下的快照读。

Snapshot Read vs Current Read

先分清:MVCC 只管快照读

语句读方式说明
SELECT ...快照读走 ReadView 判定,不加锁、不等待,可能读到历史版本
SELECT ... FOR UPDATE / FOR SHARE当前读最新已提交版本,加 X / S 锁(8.0 前 LOCK IN SHARE MODE)
UPDATE / DELETE当前读必须基于最新版本修改,加 X 锁
INSERT当前读写入新版本(insert undo 只服务回滚,不服务快照读)

SERIALIZABLE:普通 SELECT 变当前读

手册:autocommit 关闭时,"InnoDB implicitly converts all plain SELECT statements to SELECT ... FOR SHARE"。autocommit 开启时每个 SELECT 是独立只读事务,仍可走一致性读。

READ UNCOMMITTED:无一致性

手册:SELECT 非锁定,"but a possible earlier version of a row might be used"——读到哪个版本没有视图保证,即脏读。行为像 RC 但不一致。

答题口径:"MVCC 在 InnoDB 中表现为 RC/RR 下的一致性非锁定读;DML 和锁定读都是当前读,读最新已提交版本并加锁,不走版本链判定。"
快照读和当前读的分界是 MVCC 的第一道门槛。普通 SELECT 是快照读;FOR UPDATE、FOR SHARE、UPDATE、DELETE、INSERT 都是当前读,读最新已提交版本并加锁。两个特例:SERIALIZABLE 在 autocommit 关闭时把普通 SELECT 隐式转成 FOR SHARE,等于放弃 MVCC;RU 下不加锁但读到哪个版本没有保证,是脏读。记住这句答题口径,后面讲幻读和边界都会反复用到。

Overview · One Snapshot Read

一次快照读的完整旅程

快照读流程:ReadView 判定与 undo 版本链回溯 普通 SELECT 构建 ReadView,取聚簇索引当前行的 DB_TRX_ID 做可见性判定;不可见则沿 DB_ROLL_PTR 回溯 undo log 旧版本重判,返回最早可见的版本;purge 在无视图引用后物理回收旧版本。 DB_ROLL_PTR DB_ROLL_PTR 读 DB_TRX_ID 不可见 → 沿链回退 普通 SELECT 快照读 · 不加锁 ReadView · 快照视图 m_ids 活跃事务集合 min_trx_id 低水位 max_trx_id 高水位 creator 本事务 id RC 每读新建 · RR 首读建立 changes_visible(trx_id) ① = creator → 可见 ② < min → 可见 ③ ≥ max → 不可见 ④ ∈ m_ids? 活跃:不可见:可见 返回沿链找到的最早可见版本 聚簇索引当前行(最新) DB_TRX_ID=30 · DB_ROLL_PTR → id=1 · name='v3' undo log 记录 · v2 trx_id=20 · roll_ptr → name='v2'(事务 30 更新前的版本) undo log 记录 · v1 trx_id=10 · 链尾 name='v1'(事务 20 更新前的版本) 最新版本恒在 聚簇索引 聚簇索引记录原地更新, 隐藏列指向 undo 旧版本 旧版本存于 rollback segment undo 表空间中;undo 记录 物理尺寸通常小于数据行 purge → 后台物理回收 没有任何视图还需要某条 update undo 时才可清理
这页是全景:普通 SELECT 先拿一个 ReadView,然后取聚簇索引当前行的 DB_TRX_ID 走四步判定,可见直接返回;不可见就沿 DB_ROLL_PTR 回溯到 undo log 里的旧版本重判,直到找到第一个可见版本。右下角注意 purge:旧版本不是永久保留的,当没有任何视图还需要某条 update undo 时就物理回收。左边查询链和右边存储链的协作,就是 MVCC 的全部运行时图景。

Hidden Columns · §17.3

每行数据背后的三个隐藏字段

DB_TRX_ID · 6 字节

最后插入或更新该行的事务 id。删除被内部当作更新处理:置行上的特殊位作为删除标记。它是与 ReadView 对话的唯一坐标。

DB_ROLL_PTR · 7 字节

回滚指针,指向 rollback segment 中的一条 undo log 记录;该记录包含重建"更新前内容"所需的全部信息——版本链的钥匙。

DB_ROW_ID · 6 字节

随插入单调递增的行 id。仅当 InnoDB 自动生成聚簇索引(无主键且无非空唯一索引)时作为索引键;否则不出现在任何索引中。

追问答案
这三个字段在二级索引记录里吗?不在。二级索引记录不含隐藏列、也不原地更新(见第 14 页)——可见性判定必须回聚簇索引
delete 的行去哪了?内部按 update 处理:置删除标记位,DB_TRX_ID 改为删除事务的 id;物理删除由 purge 完成
无主键表会怎样?InnoDB 自动以 DB_ROW_ID 建聚簇索引——"表必须有主键"的底层原因之一(显式主键可控且不占额外列)
版本判定的坐标系:所有可见性讨论都在 trx_id 这一个单调递增的维度上展开——ReadView 的四个字段、可见性四步判定,全部围绕"行的 trx_id 落在视图的哪个区间"。
隐藏列是 MVCC 的物理基础。三个字段六个字节、七字节、六字节:TRX_ID 记录最后写这行的事务,ROLL_PTR 指向 undo 里的前镜像,ROW_ID 只在自动建聚簇索引时当索引用。两个高频追问:二级索引记录没有这些字段,所以可见性要回聚簇索引判;delete 不是真删,是打标记的 update,真删等 purge。所有可见性逻辑都在 trx_id 这个单调维度上展开,这句话是后面算法页的引子。

Undo Version Chain · §17.3

版本链:undo 记录串起历史版本

undo log 版本链结构与回溯方向 trx_id 单调递增;聚簇索引当前行通过 DB_ROLL_PTR 指向 undo 记录 v2,v2 再指向 v1;ReadView 判定不可见时沿链向旧版本回溯。 trx_id 全局单调递增:越新的事务 id 越大 · 新版本在聚簇索引,旧版本在 undo DB_ROLL_PTR DB_ROLL_PTR ReadView 判定不可见 → 逐版回溯 undo log 记录 · v1(最早版本) trx_id=10 roll_ptr = NULL · 链尾 name='v1'(插入时的初版) undo log 记录 · v2 trx_id=20 roll_ptr → v1 name='v2'(事务 30 更新前的版本) 聚簇索引当前行(最新版) DB_TRX_ID=30 DB_ROLL_PTR → v2 name='v3'(事务 30 刚写入)
undo 分类服务谁何时可丢弃
insert undo log仅事务回滚(没有别的视图会读插入的新版本)事务提交即可丢弃(手册:discarded as soon as the transaction commits)
update undo log回滚 + 一致性读的版本链须等"没有任何快照还可能需要它"——由 purge 判断
版本链怎么串起来:每条 undo 记录自带 trx_id 和指向更旧版本的 roll_ptr,聚簇索引当前行的 ROLL_PTR 指向链头,整个链条按 trx_id 递增向新排列。insert undo 和 update undo 的区别必须记牢:insert undo 提交即丢,因为没人会去读一个插入出来的新版本;update undo 还要服务一致性读,必须等所有可能引用它的快照都消失才能清——这正是后面长事务问题的根源。

ReadView · read0types.h

ReadView:一张给事务定制的"可见性坐标系"

面试口径源码字段源码注释(原文意译)
m_idsids_t m_ids"Set of RW transactions that was active when this snapshot was taken"——快照时活跃的读写事务集合,有序数组
min_trx_idm_up_limit_id"The read should see all trx ids which are strictly smaller"——低水位:比它小的一律可见
max_trx_idm_low_limit_id"The read should not see any transaction with trx id ≥ this value"——高水位:创建视图时"下一个将分配"的 id
creator_trx_idm_creator_trx_id创建该视图的事务 id(自己改的当然可见)
命名陷阱(加分点):源码里 m_low_limit_id水位、m_up_limit_id水位——注释原话 "this is the 'high water mark'"。背"low=min"必错,记住"low_limit 管上界"。
// storage/innobase/include/read0types.h
//   · mysql-server 8.0 · class ReadView

m_ids        // 活跃事务集合(有序)
m_up_limit_id   // 低水位 = min(m_ids)
                //   (m_ids 为空时 = low_limit)
m_low_limit_id  // 高水位 = 下一个待分配 id
                //   ≥ 活跃事务的最大 id
m_creator_trx_id

// 判定入口:
bool changes_visible(trx_id_t id) {
  if (id < m_up_limit_id ||
      id == m_creator_trx_id) return true;
  if (id >= m_low_limit_id) return false;
  if (m_ids.empty()) return true;
  return !binary_search(m_ids, id);
}

判定逻辑与左表一一对应;下一步是"不可见沿 roll_ptr 回退重判"

ReadView 四个字段:活跃事务集合 m_ids、低水位、高水位、创建者 id。两个容易被追问的细节:第一,源码命名反直觉,low_limit_id 是高水位、up_limit_id 是低水位,面试时用 min 和 max 表述最稳;第二,高水位是创建视图时下一个将分配的事务 id,不是活跃集合的最大值——它大于等于活跃最大 id,这保证了大于等于高水位的事务一定在视图创建之后才开始。右边是 changes_visible 的源码骨架,四步判定和下一页的图完全对应。

Visibility Decision Tree

可见性四步判定:一行 trx_id 的四连问

ReadView 可见性判定决策树 对行的 DB_TRX_ID 依次问四个问题:等于创建者可见;小于低水位可见;大于等于高水位不可见;在活跃集合中不可见否则可见;不可见则沿回滚指针回退上一版本重新判定。 yes yes yes yes no 重新判定 行的 DB_TRX_ID id == creator ? 本事务自己改的版本 id < min_trx_id ? 建视图前就已提交的事务 id ≥ max_trx_id ? 建视图之后才开始的事务 id ∈ m_ids ? 建视图时还在活跃? 可见 ✓ 直接返回该版本 不可见 ✗ 该版本来自未提交 / 未来事务 沿 DB_ROLL_PTR 回滚到上一版本 → 重新走四步判定
四步判定对着图走一遍:第一问是不是自己改的;第二问是否小于低水位,即建视图前就提交了;第三问是否大于等于高水位,即建视图后才开始的;第四问是否还在活跃集合里。可见直接返回,不可见就沿回滚指针回退上一版本重判。两个边角:第四问不走二分也能答对,但 m_ids 是有序数组,源码用 binary_search;如果整条链都不可见,这一行对本视图就相当于不存在——这就是"新插入行对老快照隐形"的机制。

ReadView Timing · §17.7.2.3

RR 与 RC:唯一区别是 ReadView 何时建立

RR 与 RC 的 ReadView 建立时机对比 事务 B 在 t2 提交修改后:RR 在 t1 首次快照读建立 ReadView 并在 t2 复用,读旧值;RC 在 t2 每条快照读新建 ReadView,读新值。 共享事件:事务 B(trx_id=50)在 t2 将 a=1 改为 a=2 并提交(A 的视图中 50 ≥ max_trx_id,不可见) REPEATABLE READ(默认) t0 BEGIN t1 SELECT t2 SELECT t3 COMMIT ReadView R 建立 读 a=1 复用 R 读 a=1 ✓ 手册:all consistent reads ... read the snapshot established by the first such read READ COMMITTED t0 BEGIN t1 SELECT t2 SELECT t3 COMMIT ReadView R1 建立 读 a=1 R2 新建 读 a=2 ✓(最新已提交) 手册:each consistent read ... sets and reads its own fresh snapshot
两个常见追问:① RR 的 ReadView 不是 BEGIN 时建立——BEGIN 只开事务,第一次快照读才建;START TRANSACTION WITH CONSISTENT SNAPSHOT 才把建立时点提前到事务开始。② 想刷新 RR 的快照:提交当前事务再发起新查询(手册原话)。
RR 和 RC 在 InnoDB 里共享同一套实现,唯一区别是 ReadView 建立时机。RR 在第一次快照读时建立并整个事务复用,所以 t2 依然读 1,可重复读;RC 每条快照读都新建视图,t2 就能读到 B 已提交的 2。两个追问要备好:BEGIN 不建视图,第一次读才建,WITH CONSISTENT SNAPSHOT 是显式提前;RR 想看新数据只能提交后重开。把这张图讲清楚,RC/RR 的题基本就通关了。

Walkthrough

案例推演:同一时间线,RR 与 RC 各读到什么

时刻操作RR 读到RC 读到原因(ReadView 视角)
t1A:BEGIN;SELECT * FROM t WHERE id=1a=1a=1RR 此刻建 ReadView R;RC 建视图 R1——两者一致
t2B:UPDATE id=1 SET a=2;COMMITB 的 trx_id=50 对 A 的视图不可见(未提交 / 未来事务)
t3A:SELECT * FROM t WHERE id=1a=1a=2RR 复用 R:50 不可见 → 沿 undo 回溯读 a=1;RC 新建 R2:50 已提交且 < R2.min → 可见,读 a=2
t4A:UPDATE id=1 SET a=3;再 SELECTa=3a=3UPDATE 是当前读:基于 a=2 最新版本改成 a=3;改完 trx_id=A 自己 → 必可见

t4 蕴含的"半幻读"

RR 下 A 在 t3 看不见 a=2,t4 却基于 a=2 改出 a=3——快照读看不见的行,当前读改得到(改完自己可见)。这就是"RR 没完全解决幻读"的演示场。

答题模板

先给结论(RR 可重复读 / RC 读已提交),再补一句机制(ReadView 时机),最后主动展示 t4 这种混合场景——三层递进,面试官就不用追问了。

这个时间线是面试手推题的原型。关键在 t3 和 t4:t3 里 RR 复用视图所以读 1,RC 新建视图所以读 2,这是可重复读和读已提交的全部差异;t4 里 UPDATE 作为当前读,直接基于最新的 a=2 改成 3,改完自己可见。注意 t4 对 RR 的含义:前一刻看不见的行,这一刻改到了——快照读和当前读的裂缝,也是下一页要讲的官方边界。

Boundaries · §17.7.2.3

快照只约束 SELECT:四个官方明确的例外

① DML 是当前读,能改"看不见"的行

手册官方示例:RR 事务里 SELECT COUNT(c1) WHERE c1='xyz' 返回 0,紧接着 DELETE WHERE c1='xyz' 却删掉多行——删的是别的事务刚提交的行;自己 update 过的行立刻对自己可见

② 锁定读越快照

FOR SHARE / FOR UPDATE 读最新已提交版本并加锁。手册:想看"最新状态",要么用 RC,要么用锁定读。

③ DDL 会撕掉快照

DROP TABLE 直接失效;拷表类 ALTER TABLE 之后,新表行在快照里"不存在"——事务返回 ER_TABLE_DEF_CHANGED,须重开事务。

④ INSERT ... SELECT 的 SELECT 部分

默认 InnoDB 对它用更强的锁,且 SELECT 部分按 RC 语义执行:即使同一事务内,也每次读新快照(手册 §17.7.2.3 默认行为)。

面试表述:"RR 的可重复读只保证快照读之间一致;DML 和锁定读永远看最新已提交版本,所以会出现'看不见却改得到'。这是设计而非缺陷——DML 必须基于最新版本才能保证约束和锁的正确性。"
一致性读有四个官方明确的边界。最重要的是 DML 例外:手册给了现成例子,COUNT 返回零但 DELETE 删掉多行,因为 DML 是当前读;而且自己改过的行立刻可见。第二是锁定读直接越快照。第三是 DDL:DROP TABLE 或者拷表式 ALTER 会让快照失效,报 ER_TABLE_DEF_CHANGED。第四冷门但加分:INSERT SELECT 的 SELECT 部分默认按 RC 语义逐条新快照。把这四条讲全,说明你读过手册原文。

Phantom Rows · §17.7.1

RR 下幻读解决了吗?——分"读方式"回答

读方式出现"幻"?机制
快照读(普通 SELECT)不会整个事务复用同一 ReadView,后来插入的行 trx_id ≥ max → 沿链回溯也找不到 → "不存在"
当前读(FOR UPDATE / UPDATE / DELETE)被阻断RR 下用 next-key lock(记录锁 + 记录前的间隙锁)锁住扫描范围,别的会话插不进来
RC 下的当前读RC 禁用 gap lock(外键/重复键检查除外),间隙可自由插入 → 幻读仍在

next-key lock 的形状

手册定义:"a combination of a record lock on the index record and a gap lock on the gap before the index record"。索引值 10,11,13,20 的可行锁区间:(−∞,10]、(10,11]、(11,13]、(13,20]、(20,+∞)——最后一段靠 supremum 伪记录兜住正无穷。gap 锁"纯抑制性":只阻止插入、可共存、不分 S/X。

唯一的缝:混用读方式

先快照读看不见新行,再 UPDATE 同条件却更新了它(当前读),随后自己也能看见了——官方甚至不建议在同一 RR 事务里混用 locking 与 nonlocking SELECT:"typically in such cases you want SERIALIZABLE"。

标准答案句式:"RR 用 MVCC 消灭了快照读的幻读,用临键锁挡住了当前读的幻读;两者混用时仍有一条可见性裂缝——所以严格说 RR 是'基本解决'而非'彻底解决'。"
幻读题的正确答法是按读方式拆开。快照读的幻读被 ReadView 复用消灭;当前读的幻读被临键锁挡住,临键锁是记录锁加前面的间隙锁,间隙锁是纯抑制性的,只防插入、可共存、不分共享排他。RC 下间隙锁禁用所以幻读还在。最后那条缝:同一事务里先快照读再当前读,看不见的行改得到也看得见了。收尾句式要背:RR 是基本解决幻读,不是彻底解决。

Purge & Long Transactions · §17.3

旧版本不是免费的:purge 与长事务的危险链

purge:物理删除的真正时机

手册:"InnoDB only physically removes the corresponding row and its index records when it discards the update undo log record written for the deletion. This removal operation is called a purge。"——delete 只打标,等没有任何视图还需要对应的 update undo 时,由后台 purge 线程连行带索引记录一起清。

undo 可丢弃条件

insert undo:事务提交即可丢(没视图会读新插入版本)。
update undo:手册措辞——须等"there is no transaction present for which InnoDB has assigned a snapshot that ... could require the information":任何活跃快照都可能还引用它

长事务危险链:长事务的 ReadView 一直被引用 → update undo 全都不能清 → undo 膨胀(手册警告 rollback segment "may grow too big, filling up the undo tablespace")→ 版本链变长 → 一致性读回溯成本上升、磁盘压力变大。等量插删的批处理还会让 purge 滞后、"死行"堆积表膨胀——可调 innodb_max_purge_lag 延迟新写入以压制 purge 落后。

手册的最佳实践

"commit transactions regularly, including transactions that issue only consistent reads"——只读事务也要及时提交,否则一样钉住 update undo。

运维抓手

information_schema.INNODB_TRX 里看 trx_started 找长事务;SHOW ENGINE INNODB STATUS 的 History list length 反映待 purge 的 undo 量。

这页把 MVCC 从原理接到生产。delete 只打删除标记,物理删除叫 purge,时机是没有任何视图还需要对应的 update undo。由此推出长事务的危险链:视图一直被引用,undo 一直不能清,回滚段膨胀填满 undo 表空间,版本链变长读变慢。手册特别提醒只读事务也要及时提交,因为只读事务的快照一样钉住 update undo。运维上用 INNODB_TRX 找长事务、看 History list length 判断 purge 压力。

Secondary Indexes · §17.3

二级索引没有隐藏列,可见性怎么判?

先记住结构差异

// 聚簇索引记录(原地更新)
[ key | cols | DB_TRX_ID | DB_ROLL_PTR ]

// 二级索引记录(不原地更新)
[ index_key | PK ]   // 无隐藏列!

// 更新方式:delete-mark + 插入新记录
// 旧记录留原地,等 purge 物理删除
页级快筛:二级索引页头维护 PAGE_MAX_TRX_ID——"highest id of a trx which may have modified a record on the page",仅二级索引页有此字段(page0types.h)。用它判断"本页是否存在视图看不见的修改",多数页可免回表。

可见性判定路径

情形InnoDB 的动作
记录正常、页无新事务改动直接使用二级索引记录(覆盖索引可用)
记录 delete-marked,或页被较新事务更新过回聚簇索引:按 DB_TRX_ID 判定,必要时用 undo 重建正确版本
上述情形撞上覆盖索引查询手册:"the covering index technique is not used"——放弃覆盖、照样回表
开启 ICPWHERE 中只用索引列的部分照常下推过滤;未命中则免回表,命中(含删标记录)才回表
面试价值:这页是 MVCC 的稀缺细节。答出"二级索引不原地更新 + delete-mark 回表 + 覆盖索引会失效"三层,足以与只会 ReadView 四步的候选人拉开差距。
二级索引的 MVCC 是稀缺加分细节。结构差异是根源:二级索引记录没有隐藏列、不原地更新,更新等于打删除标记加插入新记录。判定路径分两档:页上没有新事务改动时直接用,PAGE_MAX_TRX_ID 这个页头字段就是干这个快筛的;一旦记录被删标或页被动过,就得回聚簇索引按 DB_TRX_ID 判。两个代价:覆盖索引在这种情形会失效照样回表;ICP 例外,能先过滤的免回表。

Isolation Levels · §17.7.2.1

四种隔离级别全景:MVCC 与锁各管一段

隔离级别快照 / 一致性读加锁行为幻读备注
READ UNCOMMITTED无保证——"a possible earlier version of a row might be used"SELECT 不加锁会(脏读)其余行为类似 RC
READ COMMITTED每条快照读新建 ReadView只锁索引记录;gap 禁用(外键 / 重复键检查除外);UPDATE 半一致读仅支持 row 格式 binlog
REPEATABLE READ(InnoDB 默认)第一次快照读建立,全程复用扫描加 gap / next-key lock;唯一索引唯一条件只锁记录快照读不会;当前读被临键锁阻断不建议混用两类 SELECT
SERIALIZABLE同 RRautocommit=0:普通 SELECT 隐式转 FOR SHARE不会autocommit=1 时 SELECT 是独立只读事务,仍一致性读

RC 的两件"配套"

半一致读:UPDATE 扫描遇被锁行,先返回最新已提交版本给 server 判 WHERE,不匹配则不等锁放过;② gap 锁禁用让插入更自由——两者共同减少死锁,代价是幻读与只支持 row binlog。

MyISAM 为什么没有 MVCC?

MVCC 依赖事务基础设施:trx_id、undo 版本链、ReadView、purge 全在 InnoDB 事务系统里;MyISAM 无事务、表级锁,无从谈起——这也是业务表默认选 InnoDB 的底层原因。

隔离级别全景表用于收束。RU 没有一致性保证,是脏读;RC 每读新视图、只锁记录不加间隙锁、还有半一致读这个配套优化,代价是幻读和只支持 row binlog;RR 是默认级别,第一次读建视图加临键锁;SERIALIZABLE 在 autocommit 关闭时把普通 SELECT 全转 FOR SHARE,等于放弃 MVCC。顺带回答 MyISAM:没有事务系统就没有 trx_id 和 undo,天然与 MVCC 无缘。

Interview QA · 1/2

原理 8 连问

1 · 什么是 MVCC?解决了什么问题?

多版本读写不阻塞consistent nonlocking read

InnoDB 行级多版本并发控制:更新时旧版本进 undo,快照读按 ReadView 挑可见版本、全程无锁。解决读写互斥——读不阻塞写、写不阻塞读;写写冲突仍靠行锁。

2 · 快照读和当前读的区别?哪些语句是当前读?

ReadView最新已提交

快照读 = 普通 SELECT,走版本链不加锁;当前读 = SELECT ... FOR UPDATE / FOR SHARE、UPDATE、DELETE、INSERT,读最新已提交版本并加锁。SERIALIZABLE(autocommit=0)下普通 SELECT 也变当前读。

3 · ReadView 里有什么?可见性怎么判?

m_idsmin/maxcreator

四字段:活跃事务集合 m_ids、低水位 min_trx_id、高水位 max_trx_id、creator_trx_id。四步判定:=creator 可见;<min 可见;≥max 不可见;∈m_ids 活跃则不可见否则可见;不可见沿 roll_ptr 回退重判。

4 · RR 和 RC 的本质区别?

建视图时机实现相同

唯一区别是 ReadView 时机:RR 在第一次快照读建立、全程复用 → 可重复读;RC 每条快照读新建 → 总读最新已提交。隐藏列、undo 链、判定算法完全一样。

5 · RR 的 ReadView 是 BEGIN 时创建的吗?

首读才建WITH CONSISTENT SNAPSHOT

不是。BEGIN 只开事务,第一次快照读才建(手册:established by the first such read)。START TRANSACTION WITH CONSISTENT SNAPSHOT 才是开局即建。刷新快照只能提交后重开。

6 · RR 下幻读解决了吗?

快照读解决当前读靠临键锁混用有缝

快照读:解决(复用视图,新插入行不可见)。当前读:靠 next-key lock 阻断插入。缝在混用:看不见的行 UPDATE 改得到,改完自己可见——官方不建议同一 RR 事务混用两类读。

7 · 看不见的行,UPDATE/DELETE 能改到吗?

能 · 当前读手册官方示例

能。快照只约束 SELECT,DML 是当前读。手册示例:RR 下 COUNT(c1)='xyz' 为 0,DELETE 同条件却删掉多行;UPDATE 改完 10 行后自己 COUNT 就能看到 10 行。

8 · max_trx_id 是活跃事务的最大 id 吗?

下一个待分配 idhigh water mark

不是。它是建视图时下一个将分配的事务 id(源码注释 "high water mark"),大于等于活跃集合的最大 id。所以 id ≥ max 的一定是视图创建后才启动的事务——判"未来"的依据。

QA 第一组覆盖原理主干。第三题答四步判定时最好顺手报源码函数名 changes_visible;第五题的 BEGIN 不建视图是最常见的纠正点;第八题 max_trx_id 不是活跃最大 id,是下一个待分配 id,能答出这个说明真读过源码注释而不是背博客。

Interview QA · 2/2

进阶 7 连问

9 · delete 的数据什么时候真正删除?

delete markpurge

不立即删:内部按 update 处理,行上置删除标记位,trx_id 改为删除事务。当 purge 判定"没有任何视图还需要该 update undo"时,才物理删除行与索引记录——此前快照读沿链还能读到旧版本。

10 · 长事务为什么危险?

钉住 update undo版本链变长

长事务的 ReadView 一直被引用 → update undo 全不能清 → undo 膨胀可填满 undo 表空间;版本链变长一致性读回溯变慢。手册要求只读事务也要定期提交;运维看 History list length 与 INNODB_TRX。

11 · 二级索引怎么做 MVCC?

无隐藏列delete-mark 回表PAGE_MAX_TRX_ID

二级索引记录无隐藏列、不原地更新(改 = 删标 + 插新)。删标或页被新事务动过(页头 PAGE_MAX_TRX_ID 快筛)→ 回聚簇索引按 DB_TRX_ID 判定。代价:覆盖索引失效;ICP 能先用索引过滤,未命中免回表。

12 · MyISAM 为什么没有 MVCC?

无事务系统表级锁

MVCC 依赖 trx_id、undo 版本链、ReadView、purge 这套事务基础设施,全在 InnoDB;MyISAM 无事务、表级锁,没有多版本的承载物。业务表默认 InnoDB 的底层原因即此。

13 · SERIALIZABLE 下 MVCC 还有用吗?

看 autocommit

部分在。autocommit=1:每个 SELECT 是独立只读事务,仍走一致性读;autocommit=0:普通 SELECT 隐式转 FOR SHARE,全部当前读加锁——用 MVCC 换串行化。RU 则是另一个极端:不加锁但无视图保证,脏读。

14 · RC 的半一致读(semi-consistent read)?

UPDATE 专用减少死锁

RC 下 UPDATE 扫描遇已锁定行:先返回该行最新已提交版本给 server 判 WHERE——不匹配就不等锁直接放过,匹配才回头加锁/等锁。手册明确它"greatly reduces the probability of deadlocks"。仅 RC 有。

15 · 为什么 RC 只支持 row 格式 binlog?

复制一致性

RC 下 gap 锁禁用、半一致读提前放锁,语句的加锁范围与执行顺序不受控,statement 格式回放到从库可能产生不同结果——手册直接规定 READ COMMITTED "only row-based binary logging is supported"(MIXED 自动转 row)。

16 · MVCC 和锁是什么关系?会取代锁吗?

各管一段

分工而非替代:MVCC 优化"读-写"冲突(读走无锁快照);"写-写"冲突仍靠行锁串行化;当前读的幻读还要 gap/next-key lock 补位。隔离性 = MVCC(快照)+ 锁(当前读)的组合拳。

QA 第二组是进阶题。第九第十连着答:delete 打标等 purge,长事务钉住 undo 不让 purge 干活。第十一题的二级索引是稀缺细节。第十四题半一致读要能说出"返回最新已提交版本判 WHERE,不匹配放行"这个机制。最后一题是收束题:MVCC 和锁是分工关系不是替代关系,隔离性是快照加锁的组合拳。

Related & References

相关知识点与参考

MySQL 领域 · 相关 deck(已沉淀)

InnoDB 锁体系 →(record / gap / next-key / 意向锁 + 死锁排查)
三大日志与 crash-safe →(WAL、两阶段提交、崩溃恢复)
B+ 树索引 →(与第 14 页二级索引 MVCC 呼应)
事务与 ACID →(原子性/持久性由谁保证:undo vs redo 分工)

答题串联 · 一图流

快照读 → ReadView 四步判定 → undo 版本链回溯
当前读 → 最新已提交 + 行锁(RR 再叠 next-key lock)
RC / RR → 只差 ReadView 建立时机
旧版本生命周期 → purge → 长事务治理

参考来源(本 deck 全部结论可溯源至下列一手材料)

dev.mysql.com/doc/refman/8.0/en/innodb-multi-versioning.html§17.3:隐藏列定义、insert/update undo 分类、purge、二级索引多版本与覆盖索引失效
dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html§17.7.2.3:RR/RC 快照时机、WITH CONSISTENT SNAPSHOT、DML/DDL 例外(ER_TABLE_DEF_CHANGED)
dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html§17.7.2.1:四级别锁行为、半一致读、RC 仅 row binlog、SERIALIZABLE 隐式 FOR SHARE
dev.mysql.com/doc/refman/8.0/en/innodb-locking.html§17.7.1:record / gap / next-key lock 定义、gap 纯抑制性、supremum 伪记录
github.com/mysql/mysql-server (8.0) · include/read0types.hReadView 四字段注释(high/low water mark 原文)与 changes_visible() 判定实现
github.com/mysql/mysql-server (8.0) · include/page0types.hPAGE_MAX_TRX_ID:仅二级索引页维护的"可能改动本页的最大事务 id"
收尾页给出 MySQL 领域后续沉淀清单和答题串联逻辑。这份 deck 的所有版本敏感结论都有出处:快照时机、DML 例外、锁定义出自 8.0 手册对应章节,ReadView 字段与判定算法逐行对照 read0types.h,PAGE_MAX_TRX_ID 对照 page0types.h 注释。复习时按图索骥回到一手材料,别背二手博客的转述。