Postgresql源码(55)IndexOnlyScan读取vm信息跳过扫描堆表,为什么读取vm可以不加锁?(race condition第二篇)

2022-11-30 16:11:34 浏览数 (1)

前文 《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的执行流程是什么样的。

代码语言:javascript复制
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的,可以直接用索引元组返回,不必检查堆元组
代码语言:txt复制
- 情况一子情况:当前读拿的快照不包含这个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已经早已经做完了(执行顺序参考上面总结)。

0 人点赞