Read View 创建需遍历 trx_sys->rw_trx_list 获取活跃事务 ID 并计算 min_trx_id,该过程必须加锁 trx_sys->mutex,导致高并发下出现 waiting for trx_sys mutex 等待及 SELECT 延迟毛刺。

Read View 创建时要扫描活跃事务列表
每次生成 ReadView,InnoDB 必须原子性地采集当前所有未提交事务的 ID,这个过程需要遍历内存中的事务系统(trx_sys)链表。虽然链表长度通常很短,但它是全局临界区——必须加锁(trx_sys->mutex)防止并发修改,导致短暂阻塞。
常见错误现象:SHOW ENGINE INNODB STATUS 中出现大量 waiting for trx_sys mutex 等待;高并发短事务场景下,SELECT 延迟毛刺明显升高,尤其在 RC 隔离级别下(每条 SELECT 都新建 ReadView)。
- RC 级别下,延迟集中在每次
SELECT执行前;RR 级别只在第一个SELECT时发生一次 - 如果活跃事务数超过几百个(比如长事务未提交、连接池泄漏),扫描开销会线性上升
- 该锁不与行锁冲突,但会串行化
ReadView的创建动作,成为吞吐瓶颈点
min_trx_id 和 max_trx_id 的推导需遍历事务 ID 范围
ReadView 不仅记录活跃事务 ID 列表(m_ids),还要确定 min_trx_id(最小活跃 ID)和 max_trx_id(下一个将分配的事务 ID)。后者虽可直接读取 trx_sys->max_trx_id,但前者必须遍历整个 m_ids 才能求出最小值——没有跳表或索引加速,纯 O(n) 操作。
使用场景:当业务中存在大量短平快事务(如秒杀下单),同时又有少数长时间运行的分析查询(持有事务不提交),m_ids 列表会混杂大量已结束但尚未被 purge 的事务 ID,实际遍历长度远大于当前真正“活跃”的数量。
-
max_trx_id是乐观读取,无锁;min_trx_id必须在加锁临界区内计算 - MySQL 8.0 后对
m_ids排序做了优化,但未改变其本质为内存遍历 - 该步骤不可省略:它直接决定版本链中哪些旧版本可以被安全 purge
Undo log 版本链遍历本身不在此阶段,但 ReadView 生效后立刻触发
延迟本身不来自 undo 版本链查找——那是后续数据访问时才发生的。但 ReadView 创建完成后,第一次访问某行数据时,InnoDB 必须立即用该视图判断可见性,此时才会顺着 roll_pointer 往上遍历 undo 链。如果该行近期被高频修改,版本链可能很长,而判断逻辑(比对每个版本的 trx_id 是否落在 ReadView 可见区间)是逐节点进行的。
容易踩的坑:误以为延迟全在 ReadView 构建阶段;实际上,构建只是“发令枪”,真正耗时可能藏在第一次数据行可见性判定中,尤其当该行有数十个未 purge 的历史版本时。
- 版本链长度取决于该行被更新频率 + purge 进度,和
ReadView创建无关 - 但
ReadView一旦生成,就锁定了可见性边界;后续所有对该行的快照读都复用这套判断逻辑 - 所以“微小延迟”常是两段叠加:构建
ReadView的临界区等待 + 首次命中数据行时的版本链扫描
高并发下事务 ID 分配器竞争也会间接拖慢 ReadView 生成
虽然事务 ID 分配(trx_assign_new_id)和 ReadView 创建是两个动作,但在极高压场景下,trx_sys->mutex 会被频繁争用:一边是新事务抢 ID,一边是已有事务建 ReadView。这会导致 ReadView 创建被迫排队,放大感知延迟。
性能影响:在 10k+ TPS 的 OLTP 系统中,若未开启 innodb_autoinc_lock_mode=2 或连接池配置不合理,这种争用会显著抬高 P95 SELECT 延迟,且监控上不易归因。
- 这不是 MVCC 机制本身的缺陷,而是 InnoDB 全局资源锁设计带来的副作用
- 升级到 MySQL 8.0.30+ 可缓解,其引入了分段事务 ID 分配器,降低单点争用
- 最直接的规避方式:避免在 RC 下执行高频小查询;改用 RR 并显式控制事务粒度
ReadView 的“微小延迟”不是计算密集型开销,而是由内存结构遍历 + 全局互斥锁 + 事务系统耦合性共同导致的轻量级同步等待——它安静、短暂,却在高并发下精准放大成可观测延迟。


















