MVCC的核心实现机制
-- 创建测试表CREATE TABLE mvcc_products (id SERIAL PRIMARY KEY,name VARCHAR(100) NOT NULL,price DECIMAL(10, 2) NOT NULL);-- 插入初始数据INSERT INTO mvcc_products (name, price) VALUES('Laptop', 1200.00),('Mouse', 25.00),('Keyboard', 75.00);
SELECT id, name, price, xmin, xmax, cmin, cmax, ctiddb_mvcc-# FROM mvcc_products;id | name | price | xmin | xmax | cmin | cmax | ctid----+----------+---------+------+------+------+------+-------1 | Laptop | 1200.00 | 1128 | 0 | 0 | 0 | (0,1)2 | Mouse | 25.00 | 1128 | 0 | 0 | 0 | (0,2)3 | Keyboard | 75.00 | 1128 | 0 | 0 | 0 | (0,3)(3 rows)
BEGIN;INSERT INTO mvcc_products (name, price) VALUES ('Monitor', 300.00);-- 查询新插入行的系统列SELECT id, name, price, xmin, xmax, cmin, cmax, ctidFROM mvcc_products;id | name | price | xmin | xmax | cmin | cmax | ctid----+----------+---------+------+------+------+------+-------1 | Laptop | 1200.00 | 1128 | 0 | 0 | 0 | (0,1)2 | Mouse | 25.00 | 1128 | 0 | 0 | 0 | (0,2)3 | Keyboard | 75.00 | 1128 | 0 | 0 | 0 | (0,3)4 | Monitor | 300.00 | 1132 | 0 | 0 | 0 | (0,4)(4 rows)
db_mvcc=# BEGIN;BEGINdb_mvcc=*# UPDATE mvcc_products SET price = 1150.00 WHERE name = 'Laptop';UPDATE 1db_mvcc=*# select txid_current();txid_current--------------1133(1 row)db_mvcc=*# SELECT id, name, price, xmin, xmax, cmin, cmax, ctiddb_mvcc-*# FROM mvcc_products WHERE name = 'Laptop';id | name | price | xmin | xmax | cmin | cmax | ctid----+--------+---------+------+------+------+------+-------1 | Laptop | 1150.00 | 1133 | 0 | 0 | 0 | (0,5)(1 row)db_mvcc=*# UPDATE mvcc_products SET price = 333.00 WHERE name = 'Laptop';UPDATE 1db_mvcc=*# SELECT id, name, price, xmin, xmax, cmin, cmax, ctidFROM mvcc_products WHERE name = 'Laptop';id | name | price | xmin | xmax | cmin | cmax | ctid----+--------+--------+------+------+------+------+-------1 | Laptop | 333.00 | 1133 | 0 | 1 | 1 | (0,6)(1 row)db_mvcc=*# COMMIT;COMMIT
SELECT id, name, price, xmin, xmax, cmin, cmax, ctidFROM mvcc_products ;id | name | price | xmin | xmax | cmin | cmax | ctid----+----------+--------+------+------+------+------+-------2 | Mouse | 25.00 | 1128 | 0 | 0 | 0 | (0,2)3 | Keyboard | 75.00 | 1128 | 0 | 0 | 0 | (0,3)4 | Monitor | 300.00 | 1132 | 0 | 0 | 0 | (0,4)1 | Laptop | 333.00 | 1133 | 0 | 1 | 1 | (0,6)(4 rows)
db_mvcc=# SELECT id, name, price, xmin, xmax, cmin, cmax, ctidFROM mvcc_products ;id | name | price | xmin | xmax | cmin | cmax | ctid----+----------+--------+------+------+------+------+-------2 | Mouse | 25.00 | 1128 | 1134 | 0 | 0 | (0,2)3 | Keyboard | 75.00 | 1128 | 0 | 0 | 0 | (0,3)4 | Monitor | 300.00 | 1132 | 0 | 0 | 0 | (0,4)1 | Laptop | 333.00 | 1133 | 0 | 1 | 1 | (0,6)(4 rows)
SELECT id, name, price, xmin, xmax, cmin, cmax, ctidFROM mvcc_products WHERE name = 'Mouse'; -- 返回空id | name | price | xmin | xmax | cmin | cmax | ctid----+-------+-------+------+------+------+------+-------2 | Mouse | 25.00 | 1128 | 1134 | 0 | 0 | (0,2)
delete 事务提交后,查询不到被删除的数据。这里就不贴演示结果了。
db_mvcc=# BEGIN;BEGINdb_mvcc=*# select txid_current();txid_current--------------1137(1 row)db_mvcc=*# INSERT INTO mvcc_products (name, price) VALUES ('Webcam', 50.00);INSERT 0 1db_mvcc=*# SELECT id, name, price, xmin, xmax, cmin, cmax, ctiddb_mvcc-*# FROM mvcc_products WHERE name = 'Webcam';id | name | price | xmin | xmax | cmin | cmax | ctid----+--------+-------+------+------+------+------+-------6 | Webcam | 50.00 | 1137 | 0 | 0 | 0 | (0,9)(1 row)db_mvcc=*# UPDATE mvcc_products SET price = 45.00 WHERE name = 'Webcam';UPDATE 1db_mvcc=*# SELECT id, name, price, xmin, xmax, cmin, cmax, ctiddb_mvcc-*# FROM mvcc_products WHERE name = 'Webcam';id | name | price | xmin | xmax | cmin | cmax | ctid----+--------+-------+------+------+------+------+--------6 | Webcam | 45.00 | 1137 | 0 | 1 | 1 | (0,10)(1 row)db_mvcc=*# commit;COMMITdb_mvcc=#
SELECT id, name, price, xmin, xmax, cmin, cmax, ctidFROM mvcc_products;id | name | price | xmin | xmax | cmin | cmax | ctid----+---------+--------+------+------+------+------+--------4 | Monitor | 300.00 | 1132 | 0 | 0 | 0 | (0,4)1 | Laptop | 333.00 | 1133 | 0 | 1 | 1 | (0,6)6 | Webcam | 45.00 | 1137 | 0 | 1 | 1 | (0,10)
Infomask 位标记与 CLOG 提交日志
CLOG(Commit Log) 是 PostgreSQL 维护的专用提交日志结构,记录每个事务的状态(IN_PROGRESS、COMMITTED、ABORTED、SUB_COMMITTED)。当 infomask 未设置 Hint Bits 时,系统需查询 CLOG 确定事务状态。首次查询后,Hint Bits 会被回写到元组头部,后续访问即可跳过 CLOG 查询。可通过 pageinspect 扩展的 heap_page_items 函数查看这些内部字段 。
源码定义文件 src/include/access/htup_details.h
#define HEAP_XMIN_COMMITTED 0x0100 /* t_xmin committed */#define HEAP_XMIN_INVALID 0x0200 /* t_xmin invalid/aborted */#define HEAP_XMAX_COMMITTED 0x0400 /* t_xmax committed */#define HEAP_XMAX_INVALID 0x0800 /* t_xmax invalid/aborted */#define HEAP_HOT_UPDATED 0x4000 /* tuple was HOT-updated */#define HEAP_ONLY_TUPLE 0x8000 /* this is heap-only tuple */
根本矛盾:为什么索引不存储 MVCC 信息
设计权衡:在 PostgreSQL 的架构中,堆表存储了完整的元组版本信息,而索引条目仅存储键值(Key)和指向堆元组的物理指针(TID)。这种设计并非疏忽,而是存储密度与更新成本之间的刻意权衡。如果索引也存储 MVCC 信息,那么每次事务提交或回滚都可能引发索引页面的级联更新,导致索引体积急剧膨胀并产生巨大的写放大效应。通过将可见性逻辑集中在堆表,索引可以保持相对精简的结构。
强制回表的性能代价:由于索引本身不具备判断元组可见性的能力,标准索引扫描必须执行"回表"操作。索引扫描获取到 TID 后,必须定位到对应的堆页面,读取元组头部的 t_xmin、t_xmax 以及 infomask 标记,并结合当前事务的快照进行逻辑判断 。这种机制将原本连续的索引顺序扫描变成了对堆表的随机 I/O 访问,即使索引键值完全匹配查询条件,回表产生的物理读也会显著拖慢查询速度。
索引膨胀形成机制
UPDATE 触发:产生新堆元组,分配新的 TID 索引插入:由于 TID 改变,必须在索引中插入指向新元组的新条目 旧条目残留:指向旧版本堆元组的索引条目依然留在索引页中,直到被 VACUUM 清理
无法及时清理的原因:如果存在长事务(Long-running Transactions),其快照可能仍需要访问旧版本,导致 VACUUM 无法回收这些"死条目"。此外,在高并发写入场景下,VACUUM 的清理速度可能赶不上死条目的产生速度,形成恶性循环。
原文链接 https://mp.weixin.qq.com/s/cxoE0oix6ZZPanpo9M9TBA
获取系列文章入口:





