bigywater.tech
MySQL / InnoDB · 49

MVCC、Undo Log 和 Read View

用两个真实并发会话演示MVCC一致性读的效果,再用History list length这个真实指标验证undo log到底是按什么单位在积累。

148.08不变
B提交后,A在自己事务内真实依然读到旧值
1→41
History list length真实按事务数涨,不按行数涨

做的事:两个真实会话,一个只读一个只写

session A开一个带一致性快照的事务,中途sleep;session B在A的事务还开着的时候,修改同一行数据并提交。

session ASTART TRANSACTION WITH CONSISTENT SNAPSHOT; SELECT amount FROM orders WHERE id=1; -- 第一次读 SELECT SLEEP(4); SELECT amount FROM orders WHERE id=1; -- 第二次读,此时B已经改完并提交 COMMIT; SELECT amount FROM orders WHERE id=1; -- 提交之后,新开一次读 -- session B(在A的SLEEP期间执行) UPDATE orders SET amount = 9999.99 WHERE id=1;

验证:A的事务里,B改完提交之后再读,真实还是旧值

实测 · 本机两个并发会话的真实输出
A 第一次读:           amount = 148.08
(B 执行 UPDATE ... = 9999.99 并提交)
A 第二次读(同一事务内): amount = 148.08   ← 真实没变,B的修改对A不可见
A COMMIT 之后,新读一次:  amount = 9999.99  ← 事务结束后,才真实看到新值

A的事务用START TRANSACTION WITH CONSISTENT SNAPSHOT开启时,真实建立了一个Read View,之后A在这个事务内的每一次读,都通过undo log真实还原出Read View建立那一刻的数据版本,B之后的修改和提交对A来说是"看不见"的。A提交之后,旧事务结束,新的一次读会重新建立Read View,这时候B的修改才真实变得可见。

验证:History list length真实随"提交的事务数"涨,不是随"改的行数"涨

实测 · 本机SHOW ENGINE INNODB STATUS输出
基线:                                     History list length = 0
单条UPDATE一次性改3000行(一个事务):       History list length = 1   ← 只涨了1,不是3000
A开一个长事务快照后,B执行40次独立的单行UPDATE(各自单独提交,共40个事务):
                                            History list length = 41  ← 真实涨了40

第一次真实测试容易踩的一个误区:以为History list length是按"被修改的行数"涨的。真实测下来完全不是——一条UPDATE语句哪怕真实改了3000行,只要它是同一个事务(自动提交模式下,一条语句默认就是一个事务),History list length也只真实涨1;而40条各自独立提交的单行UPDATE,因为是40个不同的事务,真实让这个数字涨了40。History list length统计的是"有多少个已提交事务的undo log,还没有被purge线程真实清理掉",单位是事务,不是行。

验证:A的事务结束后,History list length真实没有立刻降下来

实测 · 本机执行输出
A COMMIT,长事务结束:           History list length = 41
之后等待3秒:                    History list length = 41
再补充3次UPDATE+各等5秒,共约15秒:History list length = 41   ← 真实没有变化
再补充100次UPDATE,之后再等5秒:  History list length = 41   ← 依然没变
理论上A的事务一结束,之前因为A的Read View而必须保留的那些旧版本undo log,就不再有任何活跃事务需要它们了,后台的purge线程应该会把它们真实清理掉,History list length应该会降下来。但本机这次真实观测到的是:A结束后累计等了约20秒、期间还补充执行了上百次新事务,这个数字始终停在41没有变化。诚实地说,这一步本文没有验证成功——purge线程是后台异步运行的,它的触发时机和batch大小受innodb_purge_rseg_truncate_frequency等参数影响,也可能和本机这台开发机长期只有零星查询、系统整体比较"空闲"导致purge线程调度不够积极有关,具体原因没有进一步深挖,如实记录这个"没等到"的真实结果,而不是编一个"很快就降下来了"的干净结局。

分步演示:一个真实的MVCC读写时间线

每一步都是本文上面用两个真实并发会话跑出来的原始结果。

Read View怎么让A在B提交后依然读到旧值
这一步新验证的读取

    本文用到的工具

    • 本机 MySQL 26.7.0(InnoDB),两个真实并发会话,一个长事务 + 多次独立提交的UPDATE
    • START TRANSACTION WITH CONSISTENT SNAPSHOT + SHOW ENGINE INNODB STATUS里的History list length
    • 没有验证:History list length为什么在A结束后约20秒、上百次新事务之后仍未回落,purge线程具体的调度时机没有进一步深挖
    京ICP备2026067502号-1京公网安备11010502063487号