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 的辖区。

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →