SQLite的回滚日志文件的校验机制

本文深入探讨了SQLite数据库的回滚日志机制,详细解释了在不同synchronous配置下,回滚日志文件如何保证数据库的一致性和完整性。特别关注了在synchronous配置为NORMAL时,SQLite如何通过页面数据校验来检测并避免不一致状态的回滚日志文件,以防止因断电或系统崩溃造成的数据损坏。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

SQLite的回滚日志文件的作用是,当出现不完整的事务提交时(事务提交过程中,发生了断电故障或者操作系统崩溃),系统重新上电以后,SQLite使用回滚日志文件将数据库文件恢复成事务提交之前的状态,即消除不完整事务给数据库文件带来的损坏,使数据库文件对用户进程呈现一致的状态.从用户进程的角度来看,事务中的操作要么全部执行成功,要么没有对数据库文件做任何修改.

SQLite如何保证回滚日志文件的内容是正确的呢:

  • 如果SQLite配置synchronous为FULL, 则回滚日志文件将执行2次flush操作.第一次flush将回滚日志文件中页面的数据(即数据库文件写操作的原始数据)写到磁盘,第二次flush将回滚日志文件的头信息写到磁盘.回滚日志文件头信息中记录写入的页面数量.需要注意的是,回滚日志文件头信息存储于一个单独的扇区中,该扇区不会和第一次flush操作的页面对应的扇区重合,这样确保flush回滚日志文件的头信息时,不会影响其保存的数据库文件的原始数据.SQLite执行恢复流程时,可以根据回滚日志文件头信息,获得一致状态的回滚日志文件内容.synchronous配置为FULL是SQLite的默认配置.

  • 如果SQLite配置synchronous为NORMAL,SQLite将对回滚日志文件执行一次flush操作,因此就无法得到配置为FULL时的一致性保证,因此有可能回滚日志文件的头信息写到了磁盘,而数据却因为断电故障或者操作系统崩溃没有写入完成.这种情况下,磁盘上的回滚日志文件内容就是损坏的.为了检测处于不一致状态的回滚日志文件,SQLite在synchronous配置为NORMAL时,增加了页面数据的校验.在SQLite的恢复处理流程中,将会检查页面校验数据,如果发现校验错误的页面,SQLie将不会执行恢复处理流程.

下面我们分析回滚日志文件的页面校验的设计和执行细节.当向回滚日志文件写入数据库文件的原始数据时,函数pager_write:

   3578           pData2 = CODEC2(pPager, pData, pPg->pgno, 7);
   3579           cksum = pager_cksum(pPager, (u8*)pData2);
   3580           pEnd = pData2 + pPager->pageSize;
   3581           pData2 -= 4;
   3582           saved = *(u32*)pEnd;
   3583           put32bits(pEnd, cksum);
   3584           szPg = pPager->pageSize+8;
   3585           put32bits(pData2, pPg->pgno);
   3586           rc = sqlite3OsWrite(pPager->jfd, pData2, szPg);

Line3578获得页面起始位置的指针,Line3580获得页面的结束地址,Line3579计算该页面数据的校验和,Line3583从页面结束地址开始写入4字节的校验和.Line3585将页面编号写入页面起始位置前面的4个字节中.Line3584计算写请求的数据大小,这里包含一个页面的大小(用来存储数据库文件原始数据)和附加的8字节,页面尾部追加的4字节校验和起始位置之前4字节的页面编号.Line3586向回滚日志文件进行写操作.再来看一下函数pager_cksum计算页面校验的细节:

   1073 /*
   1074 ** Compute and return a checksum for the page of data.
   1075 **
   1076 ** This is not a real checksum.  It is really just the sum of the
   1077 ** random initial value and the page number.  We experimented with
   1078 ** a checksum of the entire data, but that was found to be too slow.
   1079 **
   1080 ** Note that the page number is stored at the beginning of data and
   1081 ** the checksum is stored at the end.  This is important.  If journal
   1082 ** corruption occurs due to a power failure, the most likely scenario
   1083 ** is that one end or the other of the record will be changed.  It is
   1084 ** much less likely that the two ends of the journal record will be
   1085 ** correct and the middle be corrupt.  Thus, this "checksum" scheme,
   1086 ** though fast and simple, catches the mostly likely kind of corruption.
   1087 **
   1088 ** FIX ME:  Consider adding every 200th (or so) byte of the data to the
   1089 ** checksum.  That way if a single page spans 3 or more disk sectors and
   1090 ** only the middle sector is corrupt, we will still have a reasonable
   1091 ** chance of failing the checksum and thus detecting the problem.
   1092 */
   1093 static u32 pager_cksum(Pager *pPager, const u8 *aData){
   1094   u32 cksum = pPager->cksumInit;
   1095   int i = pPager->pageSize-200;
   1096   while( i>0 ){
   1097     cksum += aData[i];
   1098     i -= 200;
   1099   }
   1100   return cksum;
   1101 }

注释说明了校验的设计思想,将页面编号和校验放在页面的两端可以更有效的检测断电或者其他崩溃导致的不完整写入.计算页面数据的校验算法本身比较简单,就是为了性能的考虑.这个校验机制的设计,除了校验算法本身存在一定概率的碰撞以外,还存在一些需要注意的地方,注释最后的提示部分,如果页面大小相当于三个或者三个以上的磁盘的扇区大小,可能存在两端的扇区写入成功,而中间的扇区写入不成功的情况.这可能也是导致校验机制无法发挥作用的情况.计算页面数据的校验时,初始值cksumInit是随机生成的,会写入回滚日志文件头信息中.SQLite的恢复流程根据校验和算法和存储在日志头中的这个初始值对页面数据进行验证.函数writeJournalHdr:

    730 static int writeJournalHdr(Pager *pPager){
    731   char zHeader[sizeof(aJournalMagic)+16];
    732   int rc;
    733 
    734   if( pPager->stmtHdrOff==0 ){
    735     pPager->stmtHdrOff = pPager->journalOff;
    736   }
    737 
    738   rc = seekJournalHdr(pPager);
    739   if( rc ) return rc;
    740 
    741   pPager->journalHdr = pPager->journalOff;
    742   pPager->journalOff += JOURNAL_HDR_SZ(pPager);
    743 
    744   /* FIX ME:
    745   **
    746   ** Possibly for a pager not in no-sync mode, the journal magic should not
    747   ** be written until nRec is filled in as part of next syncJournal().
    748   **
    749   ** Actually maybe the whole journal header should be delayed until that
    750   ** point. Think about this.
    751   */
    752   memcpy(zHeader, aJournalMagic, sizeof(aJournalMagic));
    753   /* The nRec Field. 0xFFFFFFFF for no-sync journals. */
    754   put32bits(&zHeader[sizeof(aJournalMagic)], pPager->noSync ? 0xffffffff : 0);
    755   /* The random check-hash initialiser */
    756   sqlite3Randomness(sizeof(pPager->cksumInit), &pPager->cksumInit);
    757   put32bits(&zHeader[sizeof(aJournalMagic)+4], pPager->cksumInit);

该函数写入日志头信息,Line756:757生成随机的初始值cksumInit,并写入到回滚日志文件头中.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值