6

sqlite3.36版本 btree实现(三)- journal文件备份机制

 3 years ago
source link: https://www.codedump.info/post/20211222-sqlite-btree-3-journal/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

sqlite3.36版本 btree实现(三)- journal文件备份机制

2021-12-22

在上一节中(sqlite3.36版本 btree实现(二)- 并发控制框架),已经讲解了sqlite中的并发控制机制,里面会涉及到一个“备份页面”的模块:

  • 备份所有在一个事务中会修改到的页面。
  • 出错时回滚页面内容。

里面也提到,有两种备份文件的机制:journal文件,以及WAL文件。今天首先讲解journal文件的实现,它的效率会更低一些,也正是因为这个原因后续推出了更优的WAL机制。

sqlite中,可以使用PRAGMA journal_mode来修改备份文件机制,包括以下几种:

  • delete:默认模式。在该模式下,在事务结束时,备份文件将被删除。
  • truncate:日志文件被阶段为零字节长度。
  • persist:日志文件被留在原地,但头部被重写,表明日志不再有效。
  • memory:日志记录保留在内存中,而不是磁盘上。
  • off:不保留任何备份记录。
  • wal:采用wal形式的备份文件。

其中,前面三种delete、truncate、persist都是使用journal文件来实现的备份,区别在于事务结束之后的对备份文件的处理罢了。

本节首先讲解journal文件,下一节讲解wal备份文件。

journal文件格式

journal文件的文件名规则是:与同目录的数据库文件同名,但是多了字符串“-journal”为后缀。比如数据库文件是“test.db”,那么对应的journal文件名为“test.db-journal”。

偏移量 大小 描述

0 8 文件头的magic number: 0xd9, 0xd5, 0x05, 0xf9, 0x20, 0xa1, 0x63, 0xd7

8 4 journal文件中的页面数量,如果为-1表示一直到journal文件尾

12 4 每次计算校验值时算出来的随机数

16 4 在开始备份前数据库文件的页面数量

20 4 磁盘扇区大小

24 4 journal文件中的页面大小

这里大部分的字段都自解释了,不必多做解释,唯一需要注意的是随机数,因为这是用来后续校验备份页面的字段,这将在后面结合流程来说明。

紧跟着文件头之后,journal文件还有一系列页面数据组成的内容,其中每部分的结构如下:

偏移量 大小 描述

0 4 页面编号

4 N 备份的页面内容,N以页面大小为准,其中每页面大小在文件头中定义

N+4 4 页面的校验值

由上面分析可见,整个journal文件是这样来组织的:

  • 28字节的文件头。
  • 页面数据组成的数组,其中数组每个元素的大小为:4+页面大小(N)+4。

判断页面是否已经备份

启动一个写事务的时候,可能会修改多个页面,但是这其中可能有些修改,修改的是同一个页面的内容,因此这种情况下只需要对这个页面备份一次即可。

如何知道页面是否已经被备份过?页面管理器通过一个位图数据结构来保存这个信息:

Bitvec *pInJournal; /* One bit for each page in the database file */

计算页面校验值

计算一个页面校验码的流程在函数pager_cksum中实现,其核心逻辑是:

  • 以随机算出的校验值为初始值,这个初始值就是存在journal文件头中偏移量为[12,16]的数据。
  • 从后往前遍历页面数据,每隔200字节取一个u32类型的值,累加起来。

有了这样的关联,进行数据恢复时就能马上通过文件头存储的随机数,计算出来页面的数据是否准确。

static u32 pager_cksum(Pager *pPager, const u8 *aData){
u32 cksum = pPager->cksumInit; /* Checksum value to return */
int i = pPager->pageSize-200; /* Loop counter */
// 每隔200字节算一个值累加起来
while( i>0 ){
cksum += aData[i];
i -= 200;
return cksum;

有了前面计算校验值、以位图来判断页面是否已经备份过的了解,现在开始将备份页面的流程。

每一次需要修改一个页面之前,都会调用函数pager_write,这样就能在修改之前首先备份这个页面的内容。

要区分两种不同的页面:

  • 如果页面编号比当前数据库文件的页面数量小,说明是已有页面,需要走备份页面的流程。
  • 否则,说明是新增页面,新增的页面不需要备份,只需要修改该页面的标志位是需要落盘(PGHDR_NEED_SYNC),并且放入脏页面链表即可。

第二种情况是新增页面,没有备份的需求,这里就不做解释。

这里具体解释第一种情况,即备份已有页面的流程,其主要逻辑如下:

  • 首先根据前面的pInJournal位图数据,传入页面编号,判断这个页面是否备份过,如果已经备份过,不做任何操作。
  • 否则说明需要备份页面,将进入函数pagerAddPageToRollbackJournal中将该页面内容备份写入journal文件:

    • 调用前面提到的pager_cksum函数,计算页面的校验值。
    • 按照上面解释的journal文件格式,依次写入页面编号、页面内容、第一步计算出来的校验值。
    • 由于备份了页面,所以要把这个新增的备份页面编号写入pInJournal位图数据。

备份页面的例子

我们以一个例子来说明备份页面的流程,假设写事务执行时,情况如下:

  • 当时数据库的页面数量为100,即有100个页面。
  • 写事务执行时,依次做了如下的修改:

    • 修改页面10的一处内容。
    • 修改页面20的一处内容。
    • 修改页面10的一处内容,注意这里跟第一次修改属于同一个页面的不同位置。
    • 新增页面101。

那么,对照上面的流程,这四次页面修改在调用函数pager_write时,情况是这样的:

  • 修改页面10的一处内容:由于在备份页面位图中查不到页面编号为10的页面,且页面10小于当前数据库文件的页面数量100,属于修改当前已有页面,于是将这个页面备份到journal文件,完事了之后将这个页面编号10加入位图。
  • 修改页面20的一处内容:类似的,也是备份了页面20的内容,同时将20加入位图。
  • 修改页面10的一处内容:这一次虽然也是要修改已有页面,但是由于在位图中找到这个页面编号,说明在这一次事务中已经备份过这个页面了,于是不再需要备份操作,直接返回。
  • 新增页面101:发现该页面的编号101,大于当前数据库页面数量100,属于新增页面,于是不进行备份,只是加入到脏页面链表中同时标记需要落盘。

即:在这一次写事务执行的过程中,虽然需要修改4处内容,实际备份文件两次,新增页面一次。

前面备份待修改页面的流程中,备份的页面内容只是写到了备份文件里,实际还并没有执行sync操作强制落盘,只要没有落盘就还是存在备份数据损坏的情况。

在上一节的(sqlite3.36版本 btree实现(二)- 并发控制框架),备份文件内容落盘是放在第七步做的,此时对用户空间的页面内容的修改已经完成了,不清楚这一流程的可以回头再看看上一节的内容。

具体到journal文件的机制,这一步是放在函数pager_end_transaction进行的,pager_end_transaction函数就是上面介绍的:在事务修改完毕用户空间的页面之后,被调用。

本节讲解了journal文件的实现机制,从最早的sqlite btree实现时,备份页面的机制就一直使用journal机制,从这里的分析可以看到,这种机制很“朴素”,性能也并不好,所以后续在3.7版本的sqlite中引入了更优的WAL实现机制。

本节也并没有把所有journal文件实现机制都详细描述,只是把最核心的文件结构以及备份流程做了讲解,因为并不想在这个性能不高的机制上着墨更多,有兴趣的读者可以自行阅读相关代码。


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK