## SQLite入門與分析(四)---Page Cache之事務處理(1)
寫在前面:從本章開始,將對SQLite的每個模塊進行討論。討論的順序按照我閱讀SQLite的順序來進行,由于項目的需要,以及時間關系,不能給出一個完整的計劃,但是我會先討論我認為比較重要的內容。本節討論SQLite的事務處理技術,事務處理是DBMS中最關鍵的技術,對SQLite也一樣,它涉及到并發控制,以及故障恢復,由于內容較多,分為兩節。好了,下面進入正題。
本節通過一個具體的例子來分析SQLite原子提交的實現(基于Version 3.3.6的代碼)。
CREATE TABLE episodes( id integer primary key,name text, cid int) ;
插入一條記錄:insert into episodes(name,cid) values("cat",1) ;
它經過編譯器處理后生成的虛擬機代碼如下:
~~~
sqlite> explain insert into episodes(name,cid) values("cat",1);
0|Trace|0|0|0|explain insert into episodes(name,cid) values("cat",1);|00|
1|Goto|0|12|0||00|
2|SetNumColumns|0|3|0||00|
3|OpenWrite|0|2|0||00|
4|NewRowid|0|2|0||00|
5|Null|0|3|0||00|
6|String8|0|4|0|cat|00|
7|Integer|1|5|0||00|
8|MakeRecord|3|3|6|dad|00|
9|Insert|0|6|2|episodes|0b|
10|Close|0|0|0||00|
11|Halt|0|0|0||00|
12|Transaction|0|1|0||00|
13|VerifyCookie|0|1|0||00|
14|Transaction|1|1|0||00|
15|VerifyCookie|1|0|0||00|
16|TableLock|0|2|1|episodes|00|
17|Goto|0|2|0||00|
~~~
###1、初始狀態(Initial State)
當一個數據庫連接第一次打開時,狀態如圖所示。圖中最右邊(“Disk”標注)表示保存在存儲設備中的內容。每個方框代表一個扇區。藍色的塊表示這個扇區保存了原始數據。圖中中間區域是操作系統的磁盤緩沖區。開始的時候,這些緩存是還沒有被使用,因此這些方框是空白的。圖中左邊區域顯示SQLite用戶進程的內存。因為這個數據庫連接剛剛打開,所以還沒有任何數據記錄被讀入,所以這些內存也是空的。

###2、獲取讀鎖(Acquiring A Read Lock)
在SQLite寫數據庫之前,它必須先從數據庫中讀取相關信息。比如,在插入新的數據時,SQLite會先從sqlite_master表中讀取數據庫模式(相當于數據字典),以便編譯器對INSERT語句進行分析,確定數據插入的位置。
在進行讀操作之前,必須先獲取數據庫的共享鎖(shared lock),共享鎖允許兩個或更多的連接在同一時刻讀取數據庫。但是共享鎖不允許其它連接對數據庫進行寫操作。
shared lock存在于操作系統磁盤緩存,而不是磁盤本身。文件鎖的本質只是操作系統的內核數據結構,當操作系統崩潰或掉電時,這些內核數據也會隨之消失。

###3、讀取數據
一旦得到shared lock,就可以進行讀操作。如圖所示,數據先由OS從磁盤讀取到OS緩存,然后再由OS移到用戶進程空間。一般來說,數據庫文件分為很多頁,而一次讀操作只讀取一小部分頁面。如圖,從8個頁面讀取3個頁面。

###4、獲取Reserved Lock
在對數據進行修改操作之前,先要獲取數據庫文件的Reserved Lock,Reserved Lock和shared lock的相似之處在于,它們都允許其它進程對數據庫文件進行讀操作。Reserved Lock和Shared Lock可以共存,但是只能是一個Reserved Lock和多個Shared Lock——多個Reserved Lock不能共存。所以,在同一時刻,只能進行一個寫操作。
Reserved Lock意味著當前進程(連接)想修改數據庫文件,但是還沒開始修改操作,所以其它的進程可以讀數據庫,但不能寫數據庫。

###5、創建恢復日志(Creating A Rollback Journal File)
在對數據庫進行寫操作之前,SQLite先要創建一個單獨的日志文件,然后把要修改的頁面的原始數據寫入日志。回滾日志包含一個日志頭(圖中的綠色)——記錄數據庫文件的原始大小。所以即使數據庫文件大小改變了,我們仍知道數據庫的原始大小。
從OS的角度來看,當一個文件創建時,大多數OS(Windows,Linux,Mac OS X)不會向磁盤寫入數據,新創建的文件此時位于磁盤緩存中,之后才會真正寫入磁盤。如圖,日志文件位于OS磁盤緩存中,而不是位于磁盤。

~~~
//事務指令的實現
//p1為數據庫文件的索引號---0為main database;1為temporary tables使用的文件
//p2 不為0,一個寫事務開始
case OP_Transaction: {
//數據庫的索引號
int i = pOp->p1;
//指向數據庫對應的btree
Btree *pBt;
assert( i>=0 && i<db->nDb );
assert( (p->btreeMask & (1<<i))!=0 );
//設置btree指針
pBt = db->aDb[i].pBt;
if( pBt ){
//從這里btree開始事務,主要給文件加鎖,并設置btree事務狀態
rc = sqlite3BtreeBeginTrans(pBt, pOp->p2);
if( rc==SQLITE_BUSY ){
p->pc = pc;
p->rc = rc = SQLITE_BUSY;
goto vdbe_return;
}
if( rc!=SQLITE_OK && rc!=SQLITE_READONLY /* && rc!=SQLITE_BUSY */ ){
goto abort_due_to_error;
}
}
break;
}
//開始一個事務,如果第二個參數不為0,則一個寫事務開始,否則是一個讀事務
//如果wrflag>=2,一個exclusive事務開始,此時別的連接不能訪問數據庫
int sqlite3BtreeBeginTrans(Btree *p, int wrflag){
BtShared *pBt = p->pBt;
int rc = SQLITE_OK;
btreeIntegrity(p);
/* If the btree is already in a write-transaction, or it
** is already in a read-transaction and a read-transaction
** is requested, this is a no-op.
*/
//如果b-tree處于一個寫事務;或者處于一個讀事務,一個讀事務又請求,則返回SQLITE_OK
if( p->inTrans==TRANS_WRITE || (p->inTrans==TRANS_READ && !wrflag) ){
return SQLITE_OK;
}
/* Write transactions are not possible on a read-only database */
//寫事務不能訪問只讀數據庫
if( pBt->readOnly && wrflag ){
return SQLITE_READONLY;
}
/* If another database handle has already opened a write transaction
** on this shared-btree structure and a second write transaction is
** requested, return SQLITE_BUSY.
*/
//如果數據庫已存在一個寫事務,則該寫事務請求時返回SQLITE_BUSY
if( pBt->inTransaction==TRANS_WRITE && wrflag ){
return SQLITE_BUSY;
}
do {
//如果數據庫對應btree的第一個頁面還沒讀進內存
//則把該頁面讀進內存,數據庫也相應的加read lock
if( pBt->pPage1==0 ){
//加read lock,并讀頁面到內存
rc = lockBtree(pBt);
}
if( rc==SQLITE_OK && wrflag ){
//對數據庫文件加RESERVED_LOCK鎖
rc = sqlite3pager_begin(pBt->pPage1->aData, wrflag>1);
if( rc==SQLITE_OK ){
rc = newDatabase(pBt);
}
}
if( rc==SQLITE_OK ){
if( wrflag ) pBt->inStmt = 0;
}else{
unlockBtreeIfUnused(pBt);
}
}while( rc==SQLITE_BUSY && pBt->inTransaction==TRANS_NONE &&
sqlite3InvokeBusyHandler(pBt->pBusyHandler) );
if( rc==SQLITE_OK ){
if( p->inTrans==TRANS_NONE ){
//btree的事務數加1
pBt->nTransaction++;
}
//設置btree事務狀態
p->inTrans = (wrflag?TRANS_WRITE:TRANS_READ);
if( p->inTrans>pBt->inTransaction ){
pBt->inTransaction = p->inTrans;
}
}
btreeIntegrity(p);
return rc;
}
/*
**獲取數據庫的寫鎖,發生以下情況時去除寫鎖:
** * sqlite3pager_commit() is called.
** * sqlite3pager_rollback() is called.
** * sqlite3pager_close() is called.
** * sqlite3pager_unref() is called to on every outstanding page.
** pData指向數據庫的打開的頁面,此時并不修改,僅僅只是獲取
** 相應的pager,檢查它是否處于read-lock狀態。
**如果打開的不是臨時文件,則打開日志文件.
**如果數據庫已經處于寫狀態,則do nothing
*/
int sqlite3pager_begin(void *pData, int exFlag){
PgHdr *pPg = DATA_TO_PGHDR(pData);
Pager *pPager = pPg->pPager;
int rc = SQLITE_OK;
assert( pPg->nRef>0 );
assert( pPager->state!=PAGER_UNLOCK );
//pager已經處于share狀態
if( pPager->state==PAGER_SHARED ){
assert( pPager->aInJournal==0 );
if( MEMDB ){
pPager->state = PAGER_EXCLUSIVE;
pPager->origDbSize = pPager->dbSize;
}else{
//對文件加 RESERVED_LOCK
rc = sqlite3OsLock(pPager->fd, RESERVED_LOCK);
if( rc==SQLITE_OK ){
//設置pager的狀態
pPager->state = PAGER_RESERVED;
if( exFlag ){
rc = pager_wait_on_lock(pPager, EXCLUSIVE_LOCK);
}
}
if( rc!=SQLITE_OK ){
return rc;
}
pPager->dirtyCache = 0;
TRACE2("TRANSACTION %d\n", PAGERID(pPager));
//使用日志,不是臨時文件,則打開日志文件
if( pPager->useJournal && !pPager->tempFile ){
//為pager打開日志文件,pager應該處于RESERVED或EXCLUSIVE狀態
//會向日志文件寫入header
rc = pager_open_journal(pPager);
}
}
}
return rc;
}
//創建日志文件,pager應該處于RESERVED或EXCLUSIVE狀態
static int pager_open_journal(Pager *pPager){
int rc;
assert( !MEMDB );
assert( pPager->state>=PAGER_RESERVED );
assert( pPager->journalOpen==0 );
assert( pPager->useJournal );
assert( pPager->aInJournal==0 );
sqlite3pager_pagecount(pPager);
//日志文件頁面位圖
pPager->aInJournal = sqliteMalloc( pPager->dbSize/8 + 1 );
if( pPager->aInJournal==0 ){
rc = SQLITE_NOMEM;
goto failed_to_open_journal;
}
//打開日志文件
rc = sqlite3OsOpenExclusive(pPager->zJournal, &pPager->jfd,
pPager->tempFile);
//日志文件的位置指針
pPager->journalOff = 0;
pPager->setMaster = 0;
pPager->journalHdr = 0;
if( rc!=SQLITE_OK ){
goto failed_to_open_journal;
}
/*一般來說,os此時創建的文件位于磁盤緩存,并沒有實際
**存在于磁盤,下面三個操作就是為了把結果寫入磁盤,而對于
**windows系統來說,并沒有提供相應API,所以實際上沒有意義.
*/
//fullSync操作對windows沒有意義
sqlite3OsSetFullSync(pPager->jfd, pPager->full_fsync);
sqlite3OsSetFullSync(pPager->fd, pPager->full_fsync);
/* Attempt to open a file descriptor for the directory that contains a file.
**This file descriptor can be used to fsync() the directory
**in order to make sure the creation of a new file is actually written to disk.
*/
sqlite3OsOpenDirectory(pPager->jfd, pPager->zDirectory);
pPager->journalOpen = 1;
pPager->journalStarted = 0;
pPager->needSync = 0;
pPager->alwaysRollback = 0;
pPager->nRec = 0;
if( pPager->errCode ){
rc = pPager->errCode;
goto failed_to_open_journal;
}
pPager->origDbSize = pPager->dbSize;
//寫入日志文件的header---24個字節
rc = writeJournalHdr(pPager);
if( pPager->stmtAutoopen && rc==SQLITE_OK ){
rc = sqlite3pager_stmt_begin(pPager);
}
if( rc!=SQLITE_OK && rc!=SQLITE_NOMEM ){
rc = pager_unwritelock(pPager);
if( rc==SQLITE_OK ){
rc = SQLITE_FULL;
}
}
return rc;
failed_to_open_journal:
sqliteFree(pPager->aInJournal);
pPager->aInJournal = 0;
if( rc==SQLITE_NOMEM ){
/* If this was a malloc() failure, then we will not be closing the pager
** file. So delete any journal file we may have just created. Otherwise,
** the system will get confused, we have a read-lock on the file and a
** mysterious journal has appeared in the filesystem.
*/
sqlite3OsDelete(pPager->zJournal);
}else{
sqlite3OsUnlock(pPager->fd, NO_LOCK);
pPager->state = PAGER_UNLOCK;
}
return rc;
}
/*寫入日志文件頭
**journal header的格式如下:
** - 8 bytes: 標志日志文件的魔數
** - 4 bytes: 日志文件中記錄數
** - 4 bytes: Random number used for page hash.
** - 4 bytes: 原來數據庫的大小(kb)
** - 4 bytes: 扇區大小512byte
*/
static int writeJournalHdr(Pager *pPager){
//日志文件頭
char zHeader[sizeof(aJournalMagic)+16];
int rc = seekJournalHdr(pPager);
if( rc ) return rc;
pPager->journalHdr = pPager->journalOff;
if( pPager->stmtHdrOff==0 ){
pPager->stmtHdrOff = pPager->journalHdr;
}
//設置文件指針指向header之后
pPager->journalOff += JOURNAL_HDR_SZ(pPager);
/* FIX ME:
**
** Possibly for a pager not in no-sync mode, the journal magic should not
** be written until nRec is filled in as part of next syncJournal().
**
** Actually maybe the whole journal header should be delayed until that
** point. Think about this.
*/
memcpy(zHeader, aJournalMagic, sizeof(aJournalMagic));
/* The nRec Field. 0xFFFFFFFF for no-sync journals. */
put32bits(&zHeader[sizeof(aJournalMagic)], pPager->noSync ? 0xffffffff : 0);
/* The random check-hash initialiser */
sqlite3Randomness(sizeof(pPager->cksumInit), &pPager->cksumInit);
put32bits(&zHeader[sizeof(aJournalMagic)+4], pPager->cksumInit);
/* The initial database size */
put32bits(&zHeader[sizeof(aJournalMagic)+8], pPager->dbSize);
/* The assumed sector size for this process */
put32bits(&zHeader[sizeof(aJournalMagic)+12], pPager->sectorSize);
//寫入文件頭
rc = sqlite3OsWrite(pPager->jfd, zHeader, sizeof(zHeader));
/* The journal header has been written successfully. Seek the journal
** file descriptor to the end of the journal header sector.
*/
if( rc==SQLITE_OK ){
rc = sqlite3OsSeek(pPager->jfd, pPager->journalOff-1);
if( rc==SQLITE_OK ){
rc = sqlite3OsWrite(pPager->jfd, "\000", 1);
}
}
return rc;
}
~~~
其實現過程如下圖所示:
