SQLite3原始碼學習(32) WAL日誌詳細分析
在前面2篇文章講了有關WAL日誌相關的一些基礎知識:
接下來分析一下在WAL日誌模式下,整個事務的處理機制和流程
1.原子提交
事務管理最核心的特性就是滿足原子提交特性,之前的回滾日誌模式實現了這個特性,而WAL日誌模式也實現了原子提交的特性。
在WAL日誌模式下有3個檔案,分別是:
1.資料庫檔案,檔名任意,例如"example.db"
2.WAL日誌檔案,在資料庫檔名後加-wal,例如"example.db-wal"
3.WAL-index檔案,在資料庫檔名後加-shm,例如"example.db-shm"
WAL日誌和回滾日誌最大的區別是,在WAL模式下,修改過的資料並不直接寫入到資料庫,而是先寫入到WAL日誌。過一段時間後,通過檢查點操作將WAL日誌中修改的新頁替換資料庫檔案的老頁。
WAL日誌模式下,對資料庫的操作主要有4種:
1.讀資料
2.寫資料
3.檢查點操作,把WAL日誌的新頁同步到資料庫
4. WAL-index檔案恢復操作
所謂原子提交特性,就是在寫資料寫到一半時出現系統崩潰或斷電後,事務對資料庫的修改只能處於初始狀態或完成狀態,而不能處於中間狀態。
和回滾日誌一樣,在開始一個寫事務之前,首先要有一個讀事務將要修改的頁讀到記憶體中。讀資料時,最新修改的還沒同步到資料庫的頁從WAL日誌讀取,其他的頁在資料庫中讀取,WAL-index檔案是一個共享記憶體檔案。
在把要修改的頁讀取到記憶體中後就可以對其修改,修改前需要對WAL-index檔案加上寫鎖,修改完畢後將修改的頁追加到WAL日誌的末尾(即第mxFrame幀之後),在提交事務時,在最後一幀寫入資料庫長度,把WAL新新增的幀索引和頁號記錄到WAL-index檔案中,最後更新WAL-index頭部的mxFrame欄位。
經過上一個步驟之後,事實上寫事務已經完成了。雖然這些新修改的頁沒有同步到資料庫中,但是讀取的時候會通過WAL-index檔案查詢有哪些新修改的頁在WAL檔案中還沒同步到資料庫,如果在WAL檔案中則在WAL檔案中讀取,否則從資料庫中讀取。
SQLite會定期把WAL日誌中的頁回填到資料庫中,預設是WAL到了1000幀的時候執行檢查點操作,把從nBackfill到mxFrame的頁寫回到資料庫,如果寫到一半出現異常並不會影響事務繼續正常進行,因為讀事務讀取這些頁面是在WAL日誌中讀取。在WAL日誌和資料庫同步完畢後,如果現在沒有讀事務,WAL-index頭部欄位的mxFrame復位為0,下一次向WAL日誌追加資料時從頭開始。
回寫資料庫出現異常並不影響事務的正常進行,寫WAL日誌異常頁不會對事務的原子性有什麼影響,事務只有在提交時才在WAL-index檔案中更新mxFrame欄位,如果在此前出現事務失敗,剛寫入WAL末尾的資料將會被忽略掉。如果在寫WAL-index的時候中斷,下一次開始讀事務時會檢測到頭部異常,需要根據WAL日誌的對WAL-index檔案進行恢復,WAL-index檔案出錯會影響接下來讀寫的正確性。
2.WAL的優缺點
優點:
1.併發優勢
在WAL模式中,寫資料只是向WAL末尾新增資料,而讀事務開始前,會做一個read-mark標記,只讀read-mark之前的資料,所以寫事務和讀事務完全併發互不干擾。而回滾日誌模式,在寫事務把修改提交到資料庫時會獲取獨佔鎖,阻止其他讀事務的開始,一定程度影響了讀寫的併發。
2.寫速度優勢
在回滾日誌中,寫資料到資料庫前需要先把原始資料寫入到日誌中,並對日誌刷盤,再寫記錄到日誌頭,再刷盤,最後才把資料寫入到資料庫,這裡出現了多次磁碟I/O操作,而WAL模式需一次向WAL日誌寫入資料即可,而且也能保持事務的原子性。而且寫WAL日誌都是按順序寫入的,相對於離散寫入的也更快。
缺點:
1.需要共享記憶體
在WAL模式下,需要額外提供一個WAL-index檔案,同時需要作業系統支援對該檔案的共享記憶體訪問,這就限制了所有程式訪問資料庫必須要在同一臺機器上。
2.不支援多檔案
WAL模式下沒有回滾機制,所以一個事務處理多個檔案時,並不能保證整體的原子性。而回滾日誌模式,可以把多個資料庫的日誌關聯到master日誌裡,事務恢復時可以進行整體回滾。
3.讀效能會略有下降
因為每次讀資料庫之前都會通過WAL-index檔案查詢要讀的頁是否在日誌中,會產生一些額外的損耗。
4.WAL檔案可能會很大
在讀事務一直持續進行時,一直沒有機會把WAL日誌裡的內容更新到資料庫,會使WAL檔案變得很大。
3.讀事務的實現
在開始讀資料之前,需要通過sqlite3WalBeginReadTransaction()開啟一個讀事務,並檢查此時有沒有寫事務對資料庫進行改動,如果有改動的話,清除頁快取。
下面來一步步分析實現,首先要獲取WAL-index檔案頭
rc =walIndexReadHdr(pWal, pChanged);
在這裡需要先判斷WAL-index有沒有變更,先來看一些WAL-index的頭部格式:
Bytes | Description |
0..47 | First copy of the WAL Index Information |
48..95 | Second copy of the WAL Index Information |
96..135 | Checkpoint Information and Locks |
可以看到WAL Index頭部為48位元組,後面48~95偏移位置還有一份拷貝。為什麼同一個頭部要記錄2次呢?
把前面48位元組記為h1,接下來的拷貝部分記為h2,讀是先讀h1再讀h2,而寫是先寫h2再寫h1,如果讀到的h1和h2不同,就說明在寫入WAL-index頭部出現中斷或正在寫入,此時如果無法獲取寫鎖,那需要等待將檔案頭寫完再開始,如果可以獲取寫鎖,說明是上一次出現損壞,需要對檔案頭修復。
static int walIndexTryHdr(Wal *pWal, int *pChanged){
u32 aCksum[2]; /* Checksum on the header content */
WalIndexHdr h1, h2; /* Two copies of the header content */
WalIndexHdr volatile *aHdr; /* Header in shared memory */
//walShmBarrier(pWal);保證讀取h1和h2是嚴格按照先後次序
aHdr = walIndexHdr(pWal);
memcpy(&h1, (void *)&aHdr[0], sizeof(h1));
walShmBarrier(pWal);
memcpy(&h2, (void *)&aHdr[1], sizeof(h2));
//檔案頭損壞,未初始化,校驗值不對都需要重新恢復
if( memcmp(&h1, &h2, sizeof(h1))!=0 ){
return 1; /* Dirty read */
}
if( h1.isInit==0 ){
return 1; /* Malformed header - probably all zeros */
}
walChecksumBytes(1, (u8*)&h1, sizeof(h1)-sizeof(h1.aCksum), 0, aCksum);
if( aCksum[0]!=h1.aCksum[0] || aCksum[1]!=h1.aCksum[1] ){
return 1; /* Checksum does not match */
}
……
/* The header was successfully read. Return zero. */
return 0;
}
恢復WAL-index檔案由walIndexRecover()函式實現
static int walIndexRecover(Wal *pWal){
//獲取全部型別的獨佔鎖,此時不能進行任何其他操作
iLock = WAL_ALL_BUT_WRITE + pWal->ckptLock;
nLock = SQLITE_SHM_NLOCK - iLock;
rc = walLockExclusive(pWal, iLock, nLock);
if( rc ){
return rc;
}
//校驗WAL日誌頭部,校驗不通過不恢復WAL-index的頁號索引,只初始化WAL-index頭部
……
//校驗通過時讀取WAL日誌的所有幀,將其頁號和幀號寫入到WAL-index檔案索引。
//這裡需要注意的是校驗WAL日誌每一幀的頭部時是一個迴圈檢驗的過程,即上一幀的校驗值輸出需要作為下一幀的校驗值輸入
for(iOffset=WAL_HDRSIZE; (iOffset+szFrame)<=nSize; iOffset+=szFrame){
u32 pgno; /* Database page number for frame */
u32 nTruncate; /* dbsize field from frame header */
/* Read and decode the next log frame. */
iFrame++;
//讀取WAL日誌的幀
rc = sqlite3OsRead(pWal->pWalFd, aFrame, szFrame, iOffset);
if( rc!=SQLITE_OK ) break;
//校驗讀取的幀的頭部
isValid = walDecodeFrame(pWal, &pgno, &nTruncate, aData, aFrame);
if( !isValid ) break;
//把頁號和幀號新增到索引
rc = walIndexAppend(pWal, iFrame, pgno);
if( rc!=SQLITE_OK ) break;
……
}
//完畢後釋放鎖
walUnlockExclusive(pWal, iLock, nLock);
return rc;
}
獲取WAL-index頭部資訊後,還要獲取讀鎖,如果不需要從WAL日誌中讀取時獲取0號讀鎖
if( !useWal && pInfo->nBackfill==pWal->hdr.mxFrame){
//此時WAL日誌和資料庫已經完全同步
rc = walLockShared(pWal, WAL_READ_LOCK(0));
……
}
如果日誌和資料沒有完全同步,那麼需要從1~4號讀鎖中獲取一把,每一種鎖都對應一個pInfo->aReadMark[i],這個讀標記記錄了擁有該鎖的讀事務在WAL中所能讀取的最大幀。
如果有一個鎖空閒,將該鎖的ReadMark設為mxFrame,並獲取該鎖。如果沒有鎖空閒,那麼找到ReadMark最大的鎖並獲取。
最終讀取頁面時,只檢視pInfo->nBackfill+1~pInfo->aReadMark[i]的幀是否在WAL日誌中,pInfo->nBackfill之前的幀在資料庫中讀取。在檢查點操作時,不能將pInfo->aReadMark[i]之後的幀同步到資料庫,否則會影響讀事務的正確性。
這部分加鎖的程式碼比較繁瑣,就不再貼出。
4.寫事務的實現
寫事務的實現基本全在sqlite3WalFrames()函式裡,首先要獲取一把獨佔的寫鎖。在開始寫事務之前必定開始了一個讀事務,讀取資料庫的第一頁。下面通過註釋來說明程式碼的關鍵地方,很多細節的地方略去
int sqlite3WalFrames(
Wal *pWal, /* Wal handle to write to */
int szPage, /* Database page-size in bytes */
PgHdr *pList, /* List of dirty pages to write */
Pgno nTruncate, /* Database size after this commit */
int isCommit, /* True if this is a commit */
int sync_flags /* Flags to pass to OsSync() (or 0) */
){
//檢查WAL日誌和資料庫是否完全同步,
//如果已經完全同步,獲取0號讀鎖
//將mxFrame的值設為0,即從頭開始寫WAL日誌
if( SQLITE_OK!=(rc = walRestartLog(pWal)) ){
return rc;
}
iFrame = pWal->hdr.mxFrame;
//如果這是第一幀,寫入WAL日誌頭
if( iFrame==0 ){
…….
}
//為了便於理解把這塊程式碼從頭移到這裡
// iFirst初始為0,如果WAL-index頭被改變
//則為當前事務WAL新增的第一幀
pLive = (WalIndexHdr*)walIndexHdr(pWal);
if( memcmp(&pWal->hdr, (void *)pLive, sizeof(WalIndexHdr))!=0 ){
iFirst = pLive->mxFrame+1;
}
//遍歷所有的髒頁,寫入資料
for(p=pList; p; p=p->pDirty){
int nDbSize; /* 0 normally. Positive == commit flag */
//在當前的寫事務內,可能會多次呼叫寫資料函式
//如果這一幀在之前寫過,則只寫入幀資料
//不寫入幀頭
if( iFirst && (p->pDirty || isCommit==0) ){
u32 iWrite = 0;
VVA_ONLY(rc =) sqlite3WalFindFrame(pWal, p->pgno, &iWrite);
assert( rc==SQLITE_OK || iWrite==0 );
if( iWrite>=iFirst ){
//這裡非常關鍵,記下所有重寫的幀中最小的一個
// iReCksum為開始校驗的幀,幀頭是一個連續的迴圈校驗
if( pWal->iReCksum==0 || iWrite<pWal->iReCksum ){
pWal->iReCksum = iWrite;
}
//覆蓋已經寫入的資料幀,暫時不修改幀頭,之後統一修改
……
p->flags &= ~PGHDR_WAL_APPEND;
continue;
}
}
//如果該幀沒寫過,幀號+1
iFrame++;
assert( iOffset==walFrameOffset(iFrame, szPage) );
nDbSize = (isCommit && p->pDirty==0) ? nTruncate : 0;
//這裡會寫入幀頭
rc = walWriteOneFrame(&w, p, nDbSize, iOffset);
if( rc ) return rc;
pLast = p;
iOffset += szFrame;
p->flags |= PGHDR_WAL_APPEND;
}
/* Recalculate checksums within the wal file if required. */
//事務提交時,需要從pWal->iReCksum開始重新校驗
if( isCommit && pWal->iReCksum ){
rc = walRewriteChecksums(pWal, iFrame);
if( rc ) return rc;
}
//如果最後一幀需要在幀頭寫入資料庫大小代表事務提交了
//此後如果需要提交事務,要做的事情為:
//1.將WAL日誌刷入磁碟
//2.將所有新增的幀的頁號和幀號寫入WAL-index檔案
//3.更新WAL-index檔案頭
……
}
4.檢查點的實現
檢查點就是把WAL日誌中最新的幀同步到資料庫,預設為1000幀之後同步。在同步之後可以選擇是否將日誌檔案的長度截斷為0。
檢查點需要更新的幀從從nBackfill開始到pInfo->aReadMark[i]結束,這裡程式碼通過一個迭代器,把WAL-index的每一塊記錄的幀都按照頁號排序,按照頁號從小到大更新到資料庫,如果頁號相同,選擇後面的幀,因為後面的幀比前面要新。
static int walCheckpoint(
Wal *pWal, /* Wal connection */
sqlite3 *db, /* Check for interrupts on this handle */
int eMode, /* One of PASSIVE, FULL or RESTART */
int (*xBusy)(void*), /* Function to call when busy */
void *pBusyArg, /* Context argument for xBusyHandler */
int sync_flags, /* Flags for OsSync() (or 0) */
u8 *zBuf /* Temporary buffer to use */
){
//只有nBackfill比最大有效幀小時才更新資料庫
if( pInfo->nBackfill<pWal->hdr.mxFrame ){
/* Allocate the iterator */
//迭代器把每一塊的幀按照頁號排序
//如果J<K,那麼aPgno [aList[J]] < aPgno [aList[K]]
rc = walIteratorInit(pWal, &pIter);
if( rc!=SQLITE_OK ){
return rc;
}
//獲取所有讀鎖中,最大的aReadMark的值mxSafeFrame
……
if( pInfo->nBackfill<mxSafeFrame
&& (rc = walBusyLock(pWal, xBusy, pBusyArg, WAL_READ_LOCK(0),1))==SQLITE_OK
){
//在更新資料庫時需要持有0號鎖的獨佔鎖
//0號鎖的讀事務只在資料庫中讀取資料
/* Iterate through the contents of the WAL, copying data to the db file */
while( rc==SQLITE_OK && 0==walIteratorNext(pIter, &iDbpage, &iFrame) ){
//遍歷迭代器的每一個元素,找到符號要求的頁將其更新到資料庫
……
}
……
walUnlockExclusive(pWal, WAL_READ_LOCK(0), 1);
}
}
}
5.迭代器
下面來簡要說明一下迭代器,初始化中有一個歸併排序,比較難理解,這裡稍微講一下,之前講的歸併排序是關於連結串列的,而這裡是陣列元素的排序:
// 排序目標是,如果J<K,那麼
//aContent [aList[J]] < aContent [aList[K]]
static void walMergesort(
const u32 *aContent, /* Pages in wal */
ht_slot *aBuffer, /* Buffer of at least *pnList items to use */
ht_slot *aList, /* IN/OUT: List to sort */
int *pnList /* IN/OUT: Number of elements in aList[] */
){
//遍歷迭代器的陣列元素,將每個元素都劃分到子陣列裡
for(iList=0; iList<nList; iList++){
nMerge = 1;
aMerge = &aList[iList];
// aSub[i]. aList中存的是子陣列的首地址
// aSub[i].nList中存的是子元素的個數
//aSub[0]存1個,aSub[1]存2個,aSub[2]存2^2個元素
//依次類推
//假如當前iList是9(0b1001),那麼只有在aSub[0]和aSub[3]
//中存有子陣列
for(iSub=0; iList & (1<<iSub); iSub++){
struct Sublist *p;
assert( iSub<ArraySize(aSub) );
p = &aSub[iSub];
// p->aList為子陣列的第一個元素
//歸併後,p->aList的內容經過了重新去重和排序
//結束後p->aList本身的地址賦值給了aMerge,
// nMerge為歸併後的元素個數
walMerge(aContent, p->aList, p->nList, &aMerge, &nMerge, aBuffer);
}
// aMerge是上一個子陣列的首地址
//雖然歸併後的內容經過了重新排序,但是地址沒變
aSub[iSub].aList = aMerge;
aSub[iSub].nList = nMerge;
}
//經過簡單分析,不難得出aMerge是第一個子陣列的首地址
//aMerge和接下來的aSub[iSub]繼續歸併,歸併後陣列的
//首地址仍然輸出給aMerge,p->aList更新的是內容
//排序後它的地址已經不重要了
for(iSub++; iSub<ArraySize(aSub); iSub++){
if( nList & (1<<iSub) ){
struct Sublist *p;
p = &aSub[iSub];
walMerge(aContent, p->aList, p->nList, &aMerge, &nMerge, aBuffer);
}
}
//輸出迭代器的大小
*pnList = nMerge;
}
遍歷迭代器時需要遍歷所有的塊,每一塊初始化是都已經根據頁號排好序了,找出所有塊中最小的元素
static int walIteratorNext(
WalIterator *p, /* Iterator */
u32 *piPage, /* OUT: The page number of the next page */
u32 *piFrame /* OUT: Wal frame index of next page */
){
u32 iMin; /* Result pgno must be greater than iMin */
u32 iRet = 0xFFFFFFFF; /* 0xffffffff is never a valid page number */
int i; /* For looping through segments */
iMin = p->iPrior;
assert( iMin<0xffffffff );
//這裡遍歷所有塊
for(i=p->nSegment-1; i>=0; i--){
struct WalSegment *pSegment = &p->aSegment[i];
while( pSegment->iNext<pSegment->nEntry ){
u32 iPg = pSegment->aPgno[pSegment->aIndex[pSegment->iNext]];
//在當前塊內找到大於上一次迭代的頁號
//找到之後先別急著增加pSegment->iNext
//可能iPg並不是所有塊內最小的頁,需要遍歷
//完所有的塊才知道
if( iPg>iMin ){
if( iPg<iRet ){
iRet = iPg;
*piFrame = pSegment->iZero + pSegment->aIndex[pSegment->iNext];
}
break;
}
pSegment->iNext++;
}
}
*piPage = p->iPrior = iRet;
//遍歷迭代器所有元素後返回1
return (iRet==0xFFFFFFFF);
}
6.參考資料
《SQLite Database System Design andImplementation》p.249~p.252
相關文章
- PG wal 日誌的物理儲存分析
- Zookeeper原始碼分析(二) —– zookeeper日誌原始碼
- Zookeeper原始碼分析(二) ----- zookeeper日誌原始碼
- mybaits原始碼分析--日誌模組(四)AI原始碼
- 【RocketMQ】Dledger日誌複製原始碼分析MQ原始碼
- SQLite3原始碼學習(33) Pager模組中的相關問題和細節SQLite原始碼
- Vben Admin 原始碼學習:狀態管理-錯誤日誌原始碼
- 非易失性WAL BUFFER解析:WAL日誌讀寫改造
- 學習日誌
- Laravel 原始碼方法執行類詳細分析Laravel原始碼
- Python 日誌列印之logging.getLogger原始碼分析Python原始碼
- 【PG】PostgreSQL 預寫日誌(WAL)、checkpoint、LSNSQL
- PG wal日誌LSN相關函式函式
- LightDB不記錄WAL日誌的表
- 獲取Tomcat更詳細的日誌Tomcat
- Mybatis日誌原始碼探究MyBatis原始碼
- Java容器原始碼學習--ArrayList原始碼分析Java原始碼
- ELK日誌分析系統 超詳細!!理論+實操講解!!
- Fabric 1.0原始碼分析(14) flogging(Fabric日誌系統)原始碼
- Laravel 原始碼環境檢測類詳細分析Laravel原始碼
- 日誌分析-apache日誌分析Apache
- 以太坊原始碼分析(32)eth-downloader-peer原始碼分析原始碼
- 11.3 學習日誌
- 日誌框架學習框架
- redux v3.7.2原始碼詳細解讀與學習之composeRedux原始碼
- Hadoop學習——Client原始碼分析Hadoopclient原始碼
- PostgreSQL 原始碼解讀(22)- 查詢語句#7(PlannedStmt結構詳解-日誌分析)SQL原始碼
- ELK日誌分析系統詳解
- PostgreSQL的xlog/Wal歸檔及日誌清理SQL
- ClickHouse(16)ClickHouse日誌引擎Log詳細解析
- java集合梳理【10】— Vector超級詳細原始碼分析Java原始碼
- 【UGUI原始碼分析】Unity遮罩之Mask詳細解讀UGUI原始碼Unity遮罩
- springboot學習日誌(二)– thymeleaf學習Spring Boot
- Git 學習日誌1Git
- Riffa學習——Linux Driver原始碼分析Linux原始碼
- Spring 註解學習 詳細程式碼示例Spring
- [日誌分析篇]-利用ELK分析jumpserver日誌-日誌拆分篇Server
- Logback詳細整理,基於springboot的日誌配置Spring Boot