前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Postgresql源码(55)IndexOnlyScan读取vm信息跳过扫描堆表,为什么读取vm可以不加锁?(race condition第二篇)

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

作者头像
mingjie
发布2022-11-30 16:11:34
3670
发布2022-11-30 16:11:34
举报
文章被收录于专栏:Postgresql源码分析

前文 《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已经早已经做完了(执行顺序参考上面总结)。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-08-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 分析
    • 1 insert场景
      • 2 delete场景
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档