PostgreSQL B+树索引---页面删除之XLOG

PostgreSQL B+树索引—页面删除之XLOG

预备知识

PostgreSQL B+树索引—页面删除

PostgreSQL重启恢复—XLOG 1.0

PostgreSQL B+树索引—分裂持久性

PostgreSQL 事务—MVCC

PostgreSQL重启恢复—Checkpoint&Redo

参考资料

Postgresql在SyncOneBuffer时,为什么可以不加锁判断页面是否为脏(race condition第三篇)

概述

在《PostgreSQL B+树索引—页面删除》中,我们阐述了PostgreSQL B*树索引页面删除的流程。还遗留了一个问题,在页面删除的第一阶段,我们可能会获取到一条由内部节点和叶子节点组成的链表。在这条链表中,叶子节点会通过high key的tid指向链头节点,如图1所示:
在这里插入图片描述

图1

在第二阶段,PostgreSQL会从链头开始将节点从其左右兄弟中删除,直至链尾(也就是叶子节点)。在这个过程中,每次删除链头,都会修改叶子节点high key的tid让其指向当前链头的孩子,让其孩子作为新的链头。那么为什么一定要让叶子节点记录链头位置呢?是因为需要通过这种方式将删除的状态记录到XLOG中,便于系统后续重启恢复。(不明白?没关系,后面会详细阐述。)那么这就引发了另外一个问题:什么情况下需要写XOLG,这便是本文重点阐述的问题。

什么情况下需要XLOG?

关于持久性和原子性

在前面的文章中,我们已经阐述过元组插入时需要的XLOG(《PostgreSQL重启恢复—XLOG 1.0》)以及B*树索引分裂时需要的XLOG(《PostgreSQL B+树索引—分裂持久性》)。这两种场景都需要XLOG,但是需要XLOG的原因却有所不同。

数据插入

在向PostgreSQL中插入一条数据后,数据库可能发生断电、宕机等异常情况,发生异常时插入的状态可能有两种:

  • 插入已经提交

    如果插入已经提交,根据事务的持久性原则,插入一旦提交,那么就说明插入必定成功,插入的数据一定在数据库中。如果在数据库断电时,插入的数据还没来得及持久化,那么在重启恢复时就需要依靠XLOG对数据进行redo操作,从而恢复数据。

  • 插入尚未提交

    如果插入未提交,那么根据事务的原子性原则,插入就需要看起来像从未发生过一样。如果在数据库断电时,插入的数据已经持久化了(只要XLOG持久化了数据就可以持久化,数据持久化的时候事务不一定提交)。那么就需要某种机制来屏蔽掉这条数据,在PostgreSQL这种屏蔽是通过在查询时的可见性判断来实现的,不会用到XLOG。(详见:《PostgreSQL 事务—MVCC》)

所以,在数据插入场景下,XLOG更多的是用于实现事务的持久性。

B*树分裂

我们先来回顾一下B*树分裂的流程,如图2所示:
在这里插入图片描述

图2

现在,我们希望向B*树中插入2,这个操作会造成block2分裂,block2的分裂大致分为三个步骤:

  1. 申请一个临时块block_temp以及block5。
  2. 将block2中的数据迁移到bock_temp和block5中。
  3. 建立block_temp和block5之间的兄弟关系。
  4. 将block_temp中的数据拷贝到block2中。
  5. 将block5与block3建立兄弟连接。
    完成上述5步骤操作后,B*树如图3所示:
    在这里插入图片描述
图3
在这5个步骤中,步骤1~3不会使原始B\*树的发生改变,也不涉及数据持久化。如果在执行步骤1~3时数据库发生断电,则不会留下任何痕迹,也无需恢复,我们再来看看步骤4和5,步骤4会用block_temp中的数据覆盖block2,步骤5会将block5链接到block2和block3之间。

现在我们假设步骤4成功执行,并且执行完成之后block2中的数据落盘了,但是在执行步骤5的时候,数据库发生了断电,那么重启之后B*树如图4所示:
在这里插入图片描述
图4看着是不是很可怕,在图2中,block_temp存放了block2左半部,然后已经将其右兄弟指向了block5,所以当block_temp中的数据覆盖了block2之后,就是图4所示的情况。图4中,block2的右兄弟指向了block5,然而block5并没有持久化,所以是不存在的,同时block3的左兄弟依然还指向block2(还没来得及指向block5)。
显然,图4所示的B*树是完全无法使用的!这是因为图4是B*树分裂的一个中间状态。要么B*树不发生分裂,其状态就是图2,要么B*完成分裂,其状态就是图3。换句话说,步骤4和5是一个原子操作,要么全做,要么全不做。那么要如何来保证步骤4和5是一个原子操作呢?答案是利用XLOG以及WAL技术,下面我们来具体讨论。

如何保证原子性

为了确保分裂的原子性,PostgreSQL需要按照如下流程来执行步骤4和5:

  1. 从左向右锁定block2、block5和block3。

    由于要对这三个块中进行修改,所以需要加写锁(锁block3的原因是要修改block3的左兄弟)。

  2. 执行前面的步骤4和步骤5。

  3. 将block2、block5和block3标记为脏页。

    标记为脏页后台持久化进程才能对数据进行落盘。

  4. 产生与步骤4、5相关的XLOG,写入Log Buffer,返回LSN。

    XLOG中涵盖了用于修改block2、block5与block3的相关信息,以及维系他们之前兄弟关系的相关信息。这些信息被记录在一条XLOG中,通过这条XLOG可以确保将B*树恢复到图3的状态(即使重启时的状态如图4所示)。

    XLOG中的具体内容参见《PostgreSQL B+树索引—分裂持久性

  5. 将返回的LSN作为block2、block5和block3的page lsn。

  6. 解锁block2、block5、block3。

上面这6个步骤非常重要,步骤的顺序非常讲究,相互之间一定不能交换顺序

下面我们来看看这6个步骤是如何确保原子性的。确保原子性的关键,在于步骤4和5。步骤4将用于恢复分裂的相关信息记录到了一条XLOG中,步骤5中将block2、block5和block3的page lsn都设置为这条XLOG的LSN,所以这条XLOG一定可以用来恢复block2、block5和block3。基于WAL的特性:相关日志落盘之前,数据页面不能落盘。也就是说,在这条XLOG落盘之前,block2、block5、block3是无法落盘的,所以如果在XLOG落盘之前数据库发生了断电,那么重启之后B*树一定如图2所示。如果XLOG落盘之后数据库发生断电,那么重启之后B*树有可能如图3所示,但是由于XLOG已经落盘,我们就可以根据XLOG将B*树恢复到图4的状态,从而确保了分裂的原子性。

至此,我们可以得出一个非常重要的结论:一条XLOG就是一个原子操作,任何需要确保原子性的地方,都需要记录XLOG

MySQL MTR

上述概念在MySQL中被称为MTR(Mini-Transaction)。因为类似B*树分裂这样的操作和事务一样也需要确保原子性,但它又不是传统意义上的用户事务,作用范围要小很多,所以叫做Mini-Transaction。一个用户事务在执行过程中会产生很多这样的MTR,只有确保了MTR的原子性才能确保整个用户事务的原子性。

页面删除

好了,在明白了上述内容之后,我们再来看看本文开头讨论的关于页面删除的问题。完成页面删除的第一阶段完成后,B*树的状态可能如图5所示,那么在删除的第二阶段,我们需要依次将block4至block1之间的所有节点从其左右兄弟中删除。那么假设在删除完block4之后,并没有将block4的孩子block3记录到block1中。那么如果删除block4之后数据库发生了断电,重启之后如何知道当前的删除操作进行到哪里了呢?所以将block4从左右兄弟中删除,与将block3记录到block1中一定是一个原子操作,需要XLOG来保证原子性!下面我们结合代码与前面讲的6个步骤,来看看中图5中删除block4的完整流程:
在这里插入图片描述

  1. 步骤1:给block1、bock5、block4、block6加写锁

    加锁顺序为:block1、bock5、block4、block6(从下至上,从左至右)。相关代码:nbtpage.c line 1615~1710(_bt_getbuf和LockBuffer为实际加锁的函数)。

  2. 步骤2:将block3记录到block1中,将block4从左右兄弟中删除

    相关代码:nbtpage.c line 1787~1809。

  3. 步骤3:将block1、bock5、block4、block6标记为脏页

    标记顺序为:block6、block4、bock5、block1(这个顺序其实无所谓的)。相关代码:nbtpage.c line 1820~1825。

  4. 步骤4:为步骤2产生XLOG,写入Log Buffer,返回LSN。
    相关代码:nbtpage.c line 1828~1871。

  5. 步骤5:将返回的LSN作为block1、bock5、block4、block6的page lsn。

    相关代码:nbtpage.c line 1877~1890(具体调用PageSetLSN宏实现)。

  6. 步骤6:解锁block6、block4、bock5

    解锁顺序:block5、bock6、block4(block1不会在这里解锁)。相关代码:nbtpage.c line 1896~1909。

关于原子操作的六个步骤

最后,再对实现原子操作的六个步骤做一些补充说明:

  1. 关于步骤4

    写XLOG是指将XLOG写入Log Buffer。这里不用等Log Buffer中的XLOG落盘。至于WAL中需要保证日志先落盘,数据才能落盘,是通过在数据落盘之前,先判断page lsn对应的XLOG是否落盘,如果没有则先将对应XLOG落盘,来实现的。具体代码:bufmgr.c line:2729。

  2. 关于步骤6

    步骤6与步骤5不能交换顺序,因为步骤5会修改页面头的page lsn,如果在此之前把锁释放了,会面临并发修改page lsn带来的一系列问题(比如page lsn可能会回退)。

  3. 关于步骤3

    步骤3与步骤4不能交换顺序,这个是最难理解的一点。在PostgreSQL写XLOG的地方,我们随处可见这样的注释:/* Must mark buffers dirty before XLogInsert */,其原因与PostgreSQL的Checkpoint相关。

    Checkpoint的原理是获取一个当前的XLOG的位置作为redopoint,然后把这个位置之前的所有脏页都落盘,完成落盘后这个redopoint就是一个安全的redopoint。在重启恢复时,都是从这个redopoint开始回放XLOG。(Checkpoint的详细实现参见:《PostgreSQL重启恢复—Checkpoint&Redo》)

    而数据的落盘会调用SyncOneBuffer来实现,这个函数会校验页面是否是脏页,如果不是脏页则直接返回,否则调用FlushBuffer进行真正的落盘操作。值得注意的是,为了提高性能,SyncOneBuffer在校验脏页的时候是不会对页面加锁的!所以,如果我们交换步骤3和步骤4的顺序,就会出现下面的问题:

    • 假设当前XLOG位置是LSN_A。
    • 现在我们向block1中插入了一条数据
    • 然后为这个插入创建了一条XLOG并写入Log Buffer,于是XLOG的当前位置就变为了LSN_B,LSN_B > LSN_A。
    • 此时后台进程执行Checkpoint,于是获取到了LSN_B作为redopoint。然后开始遍历页面,将脏页进行落盘。
    • 由于脏页检测不会加锁,所以脏页检测可以与插入操作并行。
    • 由于block1没有被标记为脏页,所以检测到block1不是脏页,不做任何落盘处理。
    • 后台进程将redopoint进行持久化,完成Checkpoint。
    • 此时如果block1中的数据尚未落盘,且数据库发生了断电。那么重启时候,会从redopoint也就是LSN_B之后向后回放日志,显然不会对之前的插入操作进行redo,从而造成数据丢失。

    所以,为了兼顾正确性和并发性,步骤3必须在步骤4之前。(相关内容也可以参考:《Postgresql在SyncOneBuffer时,为什么可以不加锁判断页面是否为脏(race condition第三篇)》)

  4. 关于SyncOneBuffer

    SyncOneBuffer负责脏页的落盘工作,在判断页面为脏页进行实际落盘之前,需要对页面进行加锁,从而防止其他进程在页面落盘时向页面中写入脏数据。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
PostgreSQL是以加州大学伯克利分校计算机系开发的POSTGRES,现在已经更名为PostgreSQL. PostgreSQL支持大部分SQL标准并且提供了许多其它现代特性:复杂查询、外键、触发器、视图、事务完整性等。PostgreSQL 是一个免费的对象-关系数据库服务器(数据库管理系统),它在灵活的 BSD-风格许可证下发行。它提供了相对其他开放源代码数据库系统(比如 MySQL 和 Firebird),和专有系统(比如 Oracle、Sybase、IBM 的 DB2 和 Microsoft SQL Server)之外的另一种选择。事实上, PostgreSQL 的特性覆盖了 SQL-2/SQL-92 和 SQL-3/SQL-99,首先,它包括了可以说是目前世界上最丰富的数据类型的支持,其中有些数据类型可以说连商业数据库都不具备, 比如 IP 类型和几何类型等;其次,PostgreSQL 是全功能的自由软件数据库,很长时间以来,PostgreSQL 是唯一支持事务、子查询、多版本并行控制系统(MVCC)、数据完整性检查等特性的唯一的一种自由软件的数据库管理系统。 Inprise 的 InterBase 以及SAP等厂商将其原先专有软件开放为自由软件之后才打破了这个唯一。最后,PostgreSQL拥有一支非常活跃的开发队伍,而且在许多黑客的努力下,PostgreSQL 的质量日益提高。从技术角度来讲,PostgreSQL 采用的是比较经典的C/S(client/server)结构,也就是一个客户端对应一个服务器端守护进程的模式,这个守护进程分析客户端来的查询请求,生成规划树,进行数据检索并最终把结果格式化输出后返回给客户端。为了便于客户端的程序的编写,由数据库服务器提供了统一的客户端 C 接口。而不同的客户端接口都是源自这个 C 接口,比如ODBC,JDBC,Python,Perl,Tcl,C/C++,ESQL等, 同时也要指出的是,PostgreSQL 对接口的支持也是非常丰富的,几乎支持所有类型的数据库客户端接口。这一点也可以说是 PostgreSQL 一大优点。本课程作为PostgreSQL数据库管理一,主要讲解以下内容: 1.     PostgreSQL 存储过程基本知识2.     PostgreSQL 用户自定义函数3.     PostgreSQL 控制结构4.     PostgreSQL 游标和存储过程5.     PostgreSQL 索引6.     PostgreSQL 视图7.     PostgreSQL 触发器8.     PostgreSQL 角色、备份和还原9.     PostgreSQL 空间管理

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值