2026-10-26·von Sijie Wang#fundamentals#storage#mvcc#transactions

mvcc

MVCC:读的人永远不用等写的人

它是干嘛的。 一行正被改,同时有人在读它。读者不能排队等写者的锁,也不能读到改了一半的行。读写混合的负载(报表查询和线上流量并行)最痛的就是这里。

机制一句话。 永不原地覆盖。更新 = 造一个新版本,并给旧版本盖上"作废"章;读者按自己的快照挑有权看的那一份。

xmin 与 xmax——两个章。 每个版本的头部带两个事务编号(这是 PostgreSQL 的真实字段名)。xmin:造出这个版本的事务。xmax:让这个版本作废的事务——删掉它、或更新取代它;没被作废时为 0。三个层不许混:作废是一个逻辑状态(退役,不是消失——空间等 vacuum 才回收);xmax 是一个字段,里面只存一个数字;那个数字是某个事务的流水号,不是"人",也不是动作本身。附带一笔:PG 还借 xmax 记行锁,靠头部标志位(infomask)与真正的作废区分;它的本职仍是作废标记。

版本链。 事务 742 更新事务 731 造的行,旧版本变成 xmin=731, xmax=742,新版本是 xmin=742, xmax=0。旧版本的 xmax 等于新版本的 xmin——这个相等就是链的钩子。

一条真实记录。 2026-10-22 本机实跑,PostgreSQL 16.2,表 accounts,id=7。INSERT 的事务是 731;普通 SELECT 带上系统列(SELECT ctid, xmin, xmax, * FROM accounts)查得:ctid (0,1), xmin 731, xmax 0, balance 100。然后开一个 Repeatable Read 会话拍好快照。UPDATE 的事务 742 提交后:新读者看到新版本 (0,2), xmin 742, xmax 0, balance 80;快照会话再查,仍是 (0,1), xmin 731, xmax 742, balance 100——旧版本还躺在原地,xmax 已被盖上作废它的 742,只是这次作废对它的快照不生效。DELETE 的事务 743 提交后:新读者 count=0;快照会话在自己提交之前一直看着 balance 100,提交之后再查也是 0 行。冻结的世界正好活了一个事务。

它如何解决问题。 写者只管盖章、生新版本,从不回头看读者;读者顺着版本链按规则挑,不拿任何读锁。读写阻塞在两侧同时消失。账是:存储(同一行多份版本并存)+ 清理(PG 的 vacuum、InnoDB 的 purge)。长事务的快照像一个 GC root——钉住它还看得见的所有旧版本,清理不敢回收,表就膨胀。这是"拿着快照不撒手"的具体价格。

InnoDB 的布局不同、思想相同:行上只留最后改它的事务号 + 回滚指针,旧版本不在原地,顺指针去 undo log 里翻出来重构。(这份 undo log 一物两用:既是回滚依据,又兼任 MVCC 的旧版本仓库。)

MVCC 不管什么。 它分开的是读与写,不仲裁写写冲突——两个写者改同一行照样靠锁排队。它也不管持久性:版本可以只活在内存里,那是 wal 的辖区;它更不管一致性按什么契约出售,那是 isolation-levels 的辖区。

über diesen eintrag

Einer von sijie's Wiki-Einträgen. Die KI auf dieser Site ist im selben Corpus verankert und antwortet in sijie's Stimme mit Zitaten zurück auf Einträge wie diesen — das Beantworten kostet sijie Geld, daher wartet sie hinter einem Code: zugriffscode eingeben →