Postgres的WAL机制

 WAL即 write ahead log,即前写日志,是故障恢复的保障;顾名思意,前写日志,

即在修改后的数据写到磁盘之前,先写相关的日志到磁盘。
WAL基于一个简单的假定,在修改数据页之前先将日志写到磁盘上,这确保重做日志

时可以恢复事务的一致性状态,而不会有部分执行的事务状态。为保证WAL,每个数据

页有个LSN标记(Log sequence number,实际上采用的是WAL的文件偏移),它指向最

近修改页面的日志记录。当缓冲区管理器(Bufmgr)写出脏页时,必须确保小于页面

LSN的xlog日志已经刷写到磁盘上了。这种机制可以提高系统性能,因为只有必要的

时候才写磁盘,而不必总等待xlog的IO操作。LSN检查只用于共享缓冲区,用于临时

表的local缓冲区不需要,因此临时表肯定没有WAL日志。

当WAL重做时,检查页面的LSN,判断当前日志记录是否已经被应用,通过判断页面的

LSN大于等于WAL记录的偏移。

通常,日志记录包含足够的信息用于redo对页面的增量更新。但这只能在文件系统和

硬件系统刷写页面的原子操作时才能正常工作,不然会产生被破坏的部分刷写状态。

而实际上原子操作的刷写页面基本是不可能的,WAL需要记录更多的信息去重构脏页

。因此,checkpoint之后的第一次修改页面的日志会复制全部页面,当redo操作时,

只需要恢复页面的拷贝即可。这比简单的数据存储更加可靠,我们可以检查WAL日志

的crc来验证其合法性。通过计算页面的原有LSN是否大于最近checkpoint点可以知道

是否是checkpoint之后的第一次页面修改。

正常的WAL操作序列如下所示:
1、对共享缓冲区加pin和排它锁;
2、进入关键代码区,START_CRIT_SECTION;
3、数据修改操作;
4、标记页面为脏页;(这个操作必须在WAL插入之前,原因参考SyncOneBuffer()的说

明)
5、创建WAL日志记录,并调用XLogInsert()插入WAL日志,并使用返回的XLOG偏移更

新页面的LSN、TLI;
6、结束关键代码区,END_CRIT_SECTION;
7、释放排它锁,并unpin;

XLogInsert中的'rdata'参数是一组指针/长度综合体,它用于指明将被写入XLog记录

的数据,以及可选的共享缓冲区ID。'rdata'数组必须包括每一个修改的缓冲区,除

非重做操作已经足以重构页面。XLogInsert中包括判断共享缓冲区自最近checkpoint

以来是否被修改,如果未修改,则整个页面内容都必须被记入日志,而不仅仅

是'rdata'指向的部分内容。

    因为XLogInsert移除了说明哪个缓冲区被全部log的rdata项,所以WAL重做时需

要检测哪个缓冲区是以这种方式log的,否则可能会误导Xlog日志记录的内容。记录

多个页面变更的Xlog日志设计时需要额外加以小心,必须确定每个"BKP"位标识什么

数据。一个棘手的例子是在HEAP_UPD在)通常关联源页,BKP(2)关联目标页,但如果

原页和目标页相同,就只有BKP(1)被设置。

   基于这个原因,也为了避免缓冲区死锁的风险,设计WAL记录时以一个页面或很少

的几个页面的原子操作为好。目前XLOG的架构不能处理多于三个缓冲区引用的WAL记

录。

    当WAL包含足够的信息可以重构页面时,rdata数组中不需要指示页面的缓冲区ID

,即使有rdata项指向缓冲区也无所谓。这是因为你不想XLogInsert去记录全局页面

内容。标准的重做方式如下所示:
    reln = XLogOpenRelation(rnode);
    buffer = XLogReadBuffer(reln, blkno, true);
    Assert(BufferIsValid(buffer));
    page = (Page)BufferGetPage(buffer);

    ... initialize the page ...
   
    PageSetLSN(page, lsn);
    PageSetTLI(page, ThisTimeLineID);
    MarkBufferDirty(buffer);
    UnlockReleaseBuffer(buffer);

    当WAL只有增量更新页面的信息时,rdata数组必须指明BufferID至少一次;否则

无法防止页面破坏(torn-page)的问题。这种方式的重做序列如下所示:
    if (record->x1_info & XLR_BKP_BLOCK_n)
 << 什么也不做,因为页面已经从日志拷贝中重写;>>

    reln = XLogOpenRelation(rnode);
    buffer = XLogReadBuffer(reln, blkno, false);
    if (!BufferIsValid(buffer))
 <<什么也不做,页面已经被删除>>
    page = (Page)BufferGetPage(buffer);
   
    if (XLByteLE(lsn, PageGetLSN(page)))
    {
 /*已经修改成功*/
        UnlockReleaseBuffer(buffer);
        return;
     }

     ... 根据日志修改数据 ...
    
    PageSetLSN(page, lsn);
    PageSetTLI(page, ThisTimeLineID);
    MarkBufferDirty(buffer);
    UnlockReleaseBuffer(buffer);

    除上面提到的之外,对多页面更新,需要能确定应用于每个页面的哪些

XLR_BKP_BLOCK_n位。如果一条WAL记录包括全部重写(fully-rewritable)和增量更新

两种属性组合,则重写页面不对XLR_BKP_BLOCK_n计数。

    基于以上的种种限制,一些复杂变化(如多级索引插入)通常需要被一组连续的多

个原子操作来描述。如果中间状态不一致怎么办呢?答案是WAL重做逻辑必须能修正

它。例如,btree索引,页面分裂需要在父btree层插入一个新的key,但因为锁的原

因,这必须用两条独立的WAL日志来记录。重做代码必须识别未完成的分裂操作,在

父btree层相对应的同时插入。如果当WAL重做结束后,仍没有对应的插入,重做代码

必须自己插入以恢复索引的一致性。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值