![PostgreSQL技术内幕:事务处理深度探索](https://wfqqreader-1252317822.image.myqcloud.com/cover/876/40107876/b_40107876.jpg)
3.1 元组上的版本信息
在PostgreSQL数据库中,每条元组都会在头部记录版本信息,它包含生成元组的事务ID、删除元组的事务ID、元组的CommandID及一些Hints信息。PostgreSQL的可见性判断函数就根据这些信息,以及clog、快照来综合判断元组的可见性。
PostgreSQL数据库采用页面存储的方式,每个表都会有多个页面,页面的组织结构如图3-1所示,它从前向后保存元组的偏移量,从后向前摆放元组,中间的部分是这个页面的空闲空间(Free Space),当空闲空间不足以保存一个元组时,这个页面会变满。
![](https://epubservercos.yuewen.com/F4E379/20862583708966906/epubprivate/OEBPS/Images/41561-00-106-01.jpg?sign=1739372965-5eeOqkUJ06FiKFCGIuTDRyTR9tKnyHdq-0-85e2a1b772631454d20a5225dd5f04fc)
图3-1 页面的组织结构
每个元组都在自己的头部保存版本信息,为了提高存储空间的利用率,这些头部信息的排列尽量考虑了字节对齐的问题,每个变量也尽量考虑了复用,例如t_cid变量在不同的生命周期中,分别用来代表cmin、cmax、xvac。
• cmin代表的是生成元组时的CommandID。
• cmax代表的是删除元组时的CommandID。
• xvac是元组被Vacuum时设置的,这时元组已经脱离了原来的事务。
如果一个元组在同一个事务中被生成和删除,那么t_cid就无法表达这个元组的生命周期。也就是说,无法同时保存生成元组的CommandID和删除元组的CommandID。在这种情况下,t_cid中记录的是ComboID。ComboID与一组{cmin,cmax}相对应,这个映射关系被保存到hash表中,因此如果t_cid中保存的是ComboID(元组头部的Hints中包含HEAP_COMBOCID标记),就需要去hash表中获得cmin或cmax。
![](https://epubservercos.yuewen.com/F4E379/20862583708966906/epubprivate/OEBPS/Images/41561-00-107-1.jpg?sign=1739372965-R65YnPwWrxc0VhZENuUsvMgQfqTtju4w-0-14f4cf48ed0d46859d357211dfe333be)
![](https://epubservercos.yuewen.com/F4E379/20862583708966906/epubprivate/OEBPS/Images/41561-00-108-1.jpg?sign=1739372965-YSym6CFftFQWLYXOMsNmTiVaHsdfsZgQ-0-7858a9da5e9fa53653f0b783a5626642)
![](https://epubservercos.yuewen.com/F4E379/20862583708966906/epubprivate/OEBPS/Images/41561-00-109-1.jpg?sign=1739372965-8ZwxhQCCzCo193nVEizxO6FAmhW9UwbQ-0-146aade8d903ab47a590bd01cc9d8d86)
为了更好地观察元组上版本信息的含义,我们来看一些示例,读者需要安装一个PostgreSQL的插件——pageinspect,该插件可以展示数据页面的内容。
新创建一个t1表,并插入一个元组,可以通过pageinspect插件读取t1表目前的数据情况。
从示例可以看出,在元组插入后,事务产生了真正的事务ID 7000,也就是产生这个元组的事务ID是7000,这个事务ID也被记录在元组的xmin中,表示该元组的生成“时间”。
这时元组还没有被删除或加锁,因此它的xmax是0,表示当前元组还是活跃状态。
从infomask也可以看出来,当前的infomask中的标记位是HEAP_XMAX_INVALID,表示还没有设置xmax值。
当前还没有infomask2的标记,infomask2中的数字2代表当前元组共有两列。
从data中可以看出,数据中存储的是两个1。
![](https://epubservercos.yuewen.com/F4E379/20862583708966906/epubprivate/OEBPS/Images/41561-00-109-2.jpg?sign=1739372965-8dYzSZ1NFOxD1OrLqGkZ3xItXy692Up6-0-91f9d1d6dea981092f177135ab1daf2b)
![](https://epubservercos.yuewen.com/F4E379/20862583708966906/epubprivate/OEBPS/Images/41561-00-110-1.jpg?sign=1739372965-lFfhsP1omzE1EZ8cVJJGDAwC6zuVjroT-0-9cfe63a1f7e1e3ae1f2d10d2c236164c)
这时候将事务做Commit操作,发现元组的状态依然没有改变。
![](https://epubservercos.yuewen.com/F4E379/20862583708966906/epubprivate/OEBPS/Images/41561-00-110-2.jpg?sign=1739372965-91OxGkqADORITzWYLjSsxtnHGVxBSTSn-0-e2e391fec32966a178ce3c8b231a53d3)
对元组做一次查询操作,发现元组的infomask的标记位中增加了HEAP_XMIN_COMMITTED标记位。这个标记位提供了元组可见性判断的快速方法。每次对元组进行查询时,如果发现元组所在的事务已经提交,就设置这个快速判断的标记位,避免每次可见性判断都去clog中查询事务状态。
![](https://epubservercos.yuewen.com/F4E379/20862583708966906/epubprivate/OEBPS/Images/41561-00-110-3.jpg?sign=1739372965-uHudT0jMD4Gov6eSpn5ulZAOYuKCjPUA-0-7d8f536f3250744a34132aa89b6ce0ee)
这时启动一个新的事务,对元组做更新操作,可以发现当前事务的事务ID发生了变化,而且在数据页面中也增加了一个新的元组。从这两个元组的data列中可以看出,它们分别是更新前的旧元组和更新后的新元组两个版本。
在旧元组中,infomask中的标记位只保留了HEAP_XMIN_COMMITTED,去掉了之前的HEAP_XMAX_INVALID,因为xmax中已经记录了删除这个元组的事务ID。在MVCC中,对元组的更新操作可以理解为对旧元组的删除和对新元组的插入。
旧元组中的infomask2则增加了HEAP_HOT_UPDATED标记,表示元组已经被更新,而且更新产生的新元组和旧元组处于同一个页面中。旧元组的HEAP_HOT_UPDATED通常和新元组的HEAP_ONLY_TUPLE成对出现。
新元组中的xmin记录的是产生新元组的事务ID,新元组刚刚产生,所以没有xmax。
新元组中的infomask的标记为HEAP_XMAX_INVALID和HEAP_UPDATED,表示这是一个由更新操作产生的新版本元组。
新元组中的infomask2中的标记位记录了HEAP_ONLY_TUPLE标记位,表示这个元组目前还只是一个HOT元组(这种情况更适用于有索引的情况,可以防止产生重复的索引项,更容易清理)。
从infomask2中可以看出当前的元组有两列。
![](https://epubservercos.yuewen.com/F4E379/20862583708966906/epubprivate/OEBPS/Images/41561-00-111-1.jpg?sign=1739372965-AwJhoaulRB00wo1QeHPIr1dxqXS9ULIY-0-90d03bf68843d5317939dc7103cdc309)
再次做更新操作,页面内目前有3个元组。
![](https://epubservercos.yuewen.com/F4E379/20862583708966906/epubprivate/OEBPS/Images/41561-00-111-2.jpg?sign=1739372965-zKCGfF7q0VEelHuDVbJH6kNz30wrN7TW-0-ad14869ba79977d42478e781bfaf602c)
第1个元组已经被删除,它的t_ctid列指向了第2个元组。
第2个元组已经被删除,它的t_ctid列指向了第3个元组。
第3个元组是当前事务元组,处于活跃状态,因此它的t_ctid列指向自己。
![](https://epubservercos.yuewen.com/F4E379/20862583708966906/epubprivate/OEBPS/Images/41561-00-112-1.jpg?sign=1739372965-FjXXdUhoVto2UPxDNGn9qdu7UbVqwfXG-0-a40cfd81b706dcce110c9166a39e7aee)
Vacuum之后发现页面已经被清空,其中:
第1个元组中的lp_flags为LP_REDIRECT,表示这个元组已经被重定向到了第3个元组。
第2个元组中的lp_flags为LP_UNUSED,表示这个槽位目前没有被使用。
第3个元组目前为正常状态。
![](https://epubservercos.yuewen.com/F4E379/20862583708966906/epubprivate/OEBPS/Images/41561-00-112-2.jpg?sign=1739372965-LihQVFEaASU7TJXaGDspy2k4VvTAlj8L-0-85d9c26b5c46b3204f59af6869639f07)
插入新的元组,可以看出新元组占用了第2个槽位。
![](https://epubservercos.yuewen.com/F4E379/20862583708966906/epubprivate/OEBPS/Images/41561-00-112-3.jpg?sign=1739372965-r1SXAKLKLNqhPscHObESwCL1EHN8taf3-0-409077dd51fe3a9fe7753d58868bfae8)