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
,并写入到回滚日志文件头中.