即在修改后的数据写到磁盘之前,先写相关的日志到磁盘。
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重做结束后,仍没有对应的插入,重做代码
必须自己插入以恢复索引的一致性。