前文 《Postgresql源码(54)visibilitymap基础功能分析》
导读1:这篇比较有意思,代码不多但是并发场景需要一定的分析,这里尝试分析并记录下背景和结果。
导读2:IndexOnlyScan访问vm页面判断如果页面的可见性为VM_ALL_VISIBLE,那么可以直接使用索引数据返回,不必去读堆页面。但是访问vm页面时没有加锁,如果出现race condition有人在并发修改vm会不会出现问题?
这里先构造背景知识,然后尝试分析:
VM_ALL_VISIBLE:当前页面所有元组都可见(都没被修改过)
背景
背景知识
- Postgresql中如果执行计划走IndexOnlyScan说明扫描的字段都在索引中了,可以不必扫描堆页面直接返回结果。
- 但PG中索引页面是没有多版本信息的,堆页面才有,如果索引对应的行删了,在继续使用索引项会不会有问题?
例子:假设表中有id=1、2、3三条数据,id上有btree索引,索引上会有三条数据ctid1、ctid2、ctid3指向这三行数据,现在执行select id from tbl where id = 3;
,假设执行计划走IndexOnlyScan,我们看看PG的执行流程是什么样的。
static TupleTableSlot *
IndexOnlyNext(IndexOnlyScanState *node)
{
...
scandesc = index_beginscan(...);
...
while ((tid = index_getnext_tid(scandesc, direction)) != NULL)
{
...
if (!VM_ALL_VISIBLE(scandesc->heapRelation,
ItemPointerGetBlockNumber(tid),
&node->ioss_VMBuffer))
{
// 索引页面指向的堆页面不满足VM_ALL_VISIBLE,也就是其中有元组修改过了
// 这里需要读堆页面并做可见性判断,拿到一条元组
}
// 索引页面指向的堆页面不VM_ALL_VISIBLE
// 直接使用索引构造返回元组slot
...
return ExecClearTuple(slot);
}
这里会发现VM_ALL_VISIBLE判断决定了返回元组slot使用索引直接构造还是要去扫描堆页面构造。
- 如果VM_ALL_VISIBLE为真,说明页面内没有修改过的元组,不会出现dead tuple,可以直接使用索引数据(这才是真的index only scan)
- 如果VM_ALL_VISIBLE为假,说明页面内修改过元组,有dead tuple,需要去扫堆页面找到可见的元组(这里虽然执行计划是index only scan,但是由于索引指向的堆元组,无法确定可见性,所以还是要去扫堆页面,是假的index only scan)
上述逻辑都比较好理解,但是问题来了,VM_ALL_VISIBLE访问VM页面时没有加锁(参考《Postgresql源码(54)visibilitymap基础功能分析》)
如果上述逻辑正在判断时,被别人修改了会不会出现问题?下面逐一分析:
分析
1 insert场景
insert执行流程简化:
代码语言:javascript复制ExecInsert
table_tuple_insert /* 【1】先插表 */
heapam_tuple_insert
heap_insert
START_CRIT_SECTION()
/* 插表 */
/* 清vm */ visibilitymap_clear
/* 写XLOG */
ExecInsertIndexTuples /* 【2】再插索引 */
index_insert
btinsert
【场景一】
假设insert一条数据,但事务还未提交时,index元组是可见的,tuple元组是不可见的。
- 如果IndexOnlyNext通过这条可见元组,走VM_ALL_VISIBLE判断时,那么一定是得到false的结果(不都可见,需要继续查堆表)为什么?
- 原因:visibilitymap_clear是先于btinsert的,能看到一条索引元组时,那么visibilitymap_clear一定已经做完了。
【场景二】
假设insert一条数据,tuple元组已经插入但是不可见的,index元组还没有来得及插入(执行过程是先插元组在插索引)。
- IndexOnlyNext肯定无法看到这条索引元组,所以不会出现问题。
2 delete场景
delete执行流程简化:注意delete并不会删索引
代码语言:javascript复制...
/* proc array lock */ GetSnapshotData
...
ExecDelete
table_tuple_delete /* 【1】删表 */
heapam_tuple_delete
heap_delete
/* 删表 */
START_CRIT_SECTION()
/* 清vm */ visibilitymap_clear
/* 写XLOG */
/* 不删索引 */
...
/* proc array lock */ 更新当前proc事务id
/* 事务提交 */
假设读取一条数据正在被删除,不管堆上的数据是否标记删除,走的索引肯定没有被删除(PG删除不管索引,索引等着vacuum删)。
这样在IndexOnlyNext通过这条元组,走VM_ALL_VISIBLE判断时,会有几种情况:
- 情况一:当前读拿的快照不包含这个delete,那么这次删除就是对我不可见的,所以这条数据对我来说还没没删,VM_ALL_VISIBLE放回true是ok的,可以直接用索引元组返回,不必检查堆元组
- 情况一子情况:当前读拿的快照不包含这个delete,但是这个delete已经visibilitymap_clear完了,只是没提交。这种情况下VM_ALL_VISIBLE返回false也是Ok的,我继续去读堆页面一定可以拿到正确的结果。情况二:当前读拿的快照包含这个delete,那这个场景下一定可以保证delete已经做完并拿proc array锁更新了自己proc的xid信息(简单的说就是我能拿到一个快照包含这个delete说明这个delete肯定已经提交了,如果他提交了那就一定拿了proc array lock锁更新proc,而我拿快照也需要proc array lock,所以这个锁就是barrier,避免我这边看到中间态)。有了这个保证,说明visibilitymap_clear已经早已经做完了(执行顺序参考上面总结)。