Undo Log版本链通过DB_ROLL_PTR指针将同一行的多个历史版本串联成单向链表,头部为最新已提交版本,遍历时依可见性规则回溯查找。
Undo Log版本链是怎么串起来的
每一行数据在InnoDB里都有两个隐藏字段:
(最近修改它的事务ID)和
(指向旧版本的指针)。每次
或
,InnoDB不会直接覆盖原数据,而是把旧值写进Undo Log,并用
把它连到上一个版本,形成一条从新到旧的单向链表。
注意:只有
产生的Undo Log在事务提交后可立刻清理,因为没“上一版本”;而
的Undo Log必须保留到所有可能用到它的Read View都失效之后——否则快照读会找不到历史版本。
版本链头部永远是最新已提交版本(不是“当前未提交”的那个),遍历时从
出发,逐级回溯
如果某次查询需要读取“事务开始时”的快照,但链上最新版的
不可见,就得顺着指针往下找,直到找到第一个满足可见性规则的版本
版本链不跨行、不跨表,只针对同一行记录;不同事务对同一行反复更新,链就变长,但不会无限增长——Purge线程会在后台异步清理无用版本
Read View生成时机和判断逻辑
在
隔离级别下,Read View只在事务中**第一次执行快照读(即普通
)时生成**;而在
下,每次
都会新建一个Read View。它不是“静态快照”,而是一套动态可见性规则。
Read View包含四个关键信息:
(当前活跃事务ID集合)、
(
中最小ID)、
(下一个待分配事务ID,非最大活跃ID)、以及创建该View的事务自身ID。
判断某行是否可见:先看它的
—— 若小于
,说明修改它的事务早已提交,可见
若
在
里,说明那事务还没提交,不可见
若
≥
,说明该版本由“未来事务”写入(ID分配是严格递增的),也不可见
其他情况(比如等于当前事务ID),则可见 —— 这就是为什么你能在自己事务里读到自己刚改的数据
为什么“可重复读”能避免不可重复读,却不一定防幻读
不可重复读的本质是:同一行数据被其他事务反复
并提交,导致你两次
看到不同值。MVCC靠固定Read View解决了这个问题——后续查询都复用第一次生成的
和水位线,旧版本只要还在链上,就能读出来。
但幻读是另一回事:它源于其他事务
了新行,而这些新行的
可能落在你的Read View可见范围内(比如比
还小),于是第二次
突然多出几条记录。
MySQL的
通过间隙锁(Gap Lock)+ Next-Key Lock 来堵住插入,不是靠MVCC本身
如果你关掉
或用了
,间隙锁会退化,幻读就容易出现
单纯依赖MVCC无法拦截
带来的新行,因为新行没有“旧版本链”,它的
就是当前事务ID,天然满足可见条件
常见误判:为什么我查不到刚提交的数据
最常遇到的不是“读到旧数据”,而是“明明提交了,怎么还查不到”。典型场景是:事务A执行
并
,事务B却在之后的
中看不到变更。
这往往不是MVCC的问题,而是事务B的隔离级别或启动时机不对:
事务B在事务A提交前就已启动,并且是
,那它的Read View已经固化,自然看不见A的新版本
事务B用了
或
——这是当前读,会跳过MVCC走聚簇索引最新版,但如果A还没提交,B会被阻塞;若A已提交,B应该能看到,看不到多半是锁冲突或死锁回滚了
检查是否意外开启了
且忘了
,或者客户端连接复用导致事务未真正结束
MVCC本身不保证“立即可见”,它只保证“按事务视图一致地可见”。真正影响“能不能读到”的,是Read View生成时刻、事务状态、以及你发的是快照读还是当前读——这点最容易被忽略。
DB_TRX_IDDB_ROLL_PTRUPDATEDELETEDB_ROLL_PTRINSERTUPDATE/DELETEDB_ROLL_PTRDB_TRX_IDREPEATABLE READSELECTREAD COMMITTEDSELECTm_idsmin_trx_idm_idsmax_trx_idDB_TRX_IDmin_trx_idDB_TRX_IDm_idsDB_TRX_IDmax_trx_idUPDATE/DELETESELECTm_idsINSERTDB_TRX_IDmin_trx_idSELECTREPEATABLE READinnodb_locks_unsafe_for_binlogREAD COMMITTEDINSERTDB_TRX_IDUPDATECOMMITSELECTREPEATABLE READSELECT ... LOCK IN SHARE MODESELECT ... FOR UPDATEAUTOCOMMIT=0COMMIT