寫在前面:本節主要談談SQLite的鎖機制,SQLite是基於鎖來實現併發控制的,所以本節的內容實際上是屬於事務處理的,但是SQLite的鎖機制實現非常的簡單而巧妙,所以在這裏單獨討論一下。如果真正理解了它,對整個事務的實現也就理解了。而要真正理解SQLite的鎖機制,最好方法就是閱讀SQLite的源碼,所以在閱讀本文時,最好能結合源碼。SQLite的鎖機制很巧妙,儘管在本節中的源碼中,我寫了很多註釋,也是我個人在研究時的一點心得,但是我發現僅僅用言語,似乎不能把問題說清楚,只有通過體會,才能真正理解SQLite的鎖機制。好了,下面進入正題。
SQLite的併發控制機制是採用加鎖的方式,實現非常簡單,但也非常的巧妙,本節將對其進行一個詳細的解剖。請仔細閱讀下圖,它可以幫助更好的理解下面的內容。
1、RESERVED LOCK
RESERVED鎖意味着進程將要對數據庫進行寫操作。某一時刻只能有一個RESERVED Lock,但是RESERVED鎖和SHARED鎖可以共存,而且可以對數據庫加新的SHARED鎖。
爲什麼要用RESERVED鎖?
主要是出於併發性的考慮。由於SQLite只有庫級排斥鎖(EXCLUSIVE LOCK),如果寫事務一開始就上EXCLUSIVE鎖,然後再進行實際的數據更新,寫磁盤操作,這會使得併發性大大降低。而SQLite一旦得到數據庫的RESERVED鎖,就可以對緩存中的數據進行修改,而與此同時,其它進程可以繼續進行讀操作。直到真正需要寫磁盤時纔對數據庫加EXCLUSIVE鎖。
2、PENDING LOCK
PENDING LOCK意味着進程已經完成緩存中的數據修改,並想立即將更新寫入磁盤。它將等待此時已經存在的讀鎖事務完成,但是不允許對數據庫加新的SHARED LOCK(這與RESERVED LOCK相區別)。
爲什麼要有PENDING LOCK?
主要是爲了防止出現寫餓死的情況。由於寫事務先要獲取RESERVED LOCK,所以可能一直產生新的SHARED LOCK,使得寫事務發生餓死的情況。
3、加鎖機制的具體實現
SQLite在pager層獲取鎖的函數如下:
//獲取一個文件的鎖,如果忙則重複該操作,
//直到busy 回調用函數返回flase,或者成功獲得鎖
static int pager_wait_on_lock(Pager *pPager, int locktype){
int rc;
assert( PAGER_SHARED==SHARED_LOCK );
assert( PAGER_RESERVED==RESERVED_LOCK );
assert( PAGER_EXCLUSIVE==EXCLUSIVE_LOCK );
if( pPager->state>=locktype ){
rc = SQLITE_OK;
}else{
//重複直到獲得鎖
do {
rc = sqlite3OsLock(pPager->fd, locktype);
}while( rc==SQLITE_BUSY && sqlite3InvokeBusyHandler(pPager->pBusyHandler) );
if( rc==SQLITE_OK ){
//設置pager的狀態
pPager->state = locktype;
}
}
return rc;
}
Windows下具體的實現如下:
static int winLock(OsFile *id, int locktype){
int rc = SQLITE_OK; /* Return code from subroutines */
int res = 1; /* Result of a windows lock call */
int newLocktype; /* Set id->locktype to this value before exiting */
int gotPendingLock = 0;/* True if we acquired a PENDING lock this time */
winFile *pFile = (winFile*)id;
assert( pFile!=0 );
TRACE5("LOCK %d %d was %d(%d)\n",
pFile->h, locktype, pFile->locktype, pFile->sharedLockByte);
/* If there is already a lock of this type or more restrictive on the
** OsFile, do nothing. Don't use the end_lock: exit path, as
** sqlite3OsEnterMutex() hasn't been called yet.
*/
//當前的鎖>=locktype,則返回
if( pFile->locktype>=locktype ){
return SQLITE_OK;
}
/* Make sure the locking sequence is correct
*/
assert( pFile->locktype!=NO_LOCK || locktype==SHARED_LOCK );
assert( locktype!=PENDING_LOCK );
assert( locktype!=RESERVED_LOCK || pFile->locktype==SHARED_LOCK );
/* Lock the PENDING_LOCK byte if we need to acquire a PENDING lock or
** a SHARED lock. If we are acquiring a SHARED lock, the acquisition of
** the PENDING_LOCK byte is temporary.
*/
newLocktype = pFile->locktype;
/*兩種情況: (1)如果當前文件處於無鎖狀態(獲取讀鎖---讀事務
**和寫事務在最初階段都要經歷的階段),
**(2)處於RESERVED_LOCK,且請求的鎖爲EXCLUSIVE_LOCK(寫事務)
**則對執行加PENDING_LOCK
*/
/////////////////////(1)///////////////////
if( pFile->locktype==NO_LOCK
|| (locktype==EXCLUSIVE_LOCK && pFile->locktype==RESERVED_LOCK)
){
int cnt = 3;
//加pending鎖
while( cnt-->0 && (res = LockFile(pFile->h, PENDING_BYTE, 0, 1, 0))==0 ){
/* Try 3 times to get the pending lock. The pending lock might be
** held by another reader process who will release it momentarily.
*/
TRACE2("could not get a PENDING lock. cnt=%d\n", cnt);
Sleep(1);
}
//設置爲gotPendingLock爲1,使和在後面要釋放PENDING鎖
gotPendingLock = res;
}
/* Acquire a shared lock
*/
/*獲取shared lock
**此時,事務應該持有PENDING鎖,而PENDING鎖作爲事務從UNLOCKED到
**SHARED_LOCKED的一個過渡,所以事務由PENDING->SHARED
**此時,實際上鎖處於兩個狀態:PENDING和SHARED,
**直到後面釋放PENDING鎖後,才真正處於SHARED狀態
*/
////////////////(2)/////////////////////////////////////
if( locktype==SHARED_LOCK && res ){
assert( pFile->locktype==NO_LOCK );
res = getReadLock(pFile);
if( res ){
newLocktype = SHARED_LOCK;
}
}
/* Acquire a RESERVED lock
*/
/*獲取RESERVED
**此時事務持有SHARED_LOCK,變化過程爲SHARED->RESERVED。
**RESERVED鎖的作用就是爲了提高系統的併發性能
*/
////////////////////////(3)/////////////////////////////////
if( locktype==RESERVED_LOCK && res ){
assert( pFile->locktype==SHARED_LOCK );
//加RESERVED鎖
res = LockFile(pFile->h, RESERVED_BYTE, 0, 1, 0);
if( res ){
newLocktype = RESERVED_LOCK;
}
}
/* Acquire a PENDING lock
*/
/*獲取PENDING鎖
**此時事務持有RESERVED_LOCK,且事務申請EXCLUSIVE_LOCK
**變化過程爲:RESERVED->PENDING。
**PENDING狀態只是唯一的作用就是防止寫餓死.
**讀事務不會執行該代碼,但是寫事務會執行該代碼,
**執行該代碼後gotPendingLock設爲0,後面就不會釋放PENDING鎖。
*/
//////////////////////////////(4)////////////////////////////////
if( locktype==EXCLUSIVE_LOCK && res ){
//這裏沒有實際的加鎖操作,只是把鎖的狀態改爲PENDING狀態
newLocktype = PENDING_LOCK;
//設置了gotPendingLock,後面就不會釋放PENDING鎖了,
//相當於加了PENDING鎖,實際上是在開始處加的PENDING鎖
gotPendingLock = 0;
}
/* Acquire an EXCLUSIVE lock
*/
/*獲取EXCLUSIVE鎖
**當一個事務執行該代碼時,它應該滿足以下條件:
**(1)鎖的狀態爲:PENDING (2)是一個寫事務
**變化過程:PENDING->EXCLUSIVE
*/
/////////////////////////(5)///////////////////////////////////////////
if( locktype==EXCLUSIVE_LOCK && res ){
assert( pFile->locktype>=SHARED_LOCK );
res = unlockReadLock(pFile);
TRACE2("unreadlock = %d\n", res);
res = LockFile(pFile->h, SHARED_FIRST, 0, SHARED_SIZE, 0);
if( res ){
newLocktype = EXCLUSIVE_LOCK;
}else{
TRACE2("error-code = %d\n", GetLastError());
}
}
/* If we are holding a PENDING lock that ought to be released, then
** release it now.
*/
/*此時事務在第2步中獲得PENDING鎖,它將申請SHARED_LOCK(第3步,和圖形相對照),
**而在之前它已經獲取了PENDING鎖,
**所以在這裏它需要釋放PENDING鎖,此時鎖的變化爲:PENDING->SHARED
*/
//////////////////////////(6)/////////////////////////////////////
if( gotPendingLock && locktype==SHARED_LOCK ){
UnlockFile(pFile->h, PENDING_BYTE, 0, 1, 0);
}
/* Update the state of the lock has held in the file descriptor then
** return the appropriate result code.
*/
if( res ){
rc = SQLITE_OK;
}else{
TRACE4("LOCK FAILED %d trying for %d but got %d\n", pFile->h,
locktype, newLocktype);
rc = SQLITE_BUSY;
}
//在這裏設置文件鎖的狀態
pFile->locktype = newLocktype;
return rc;
}
在幾個關鍵的部位標記數字。
(I)對於一個讀事務會的完整經過:
語句序列:(1)——>(2)——>(6)
相應的狀態真正的變化過程爲:UNLOCKED→PENDING(1)→PENDING、SHARED(2)→SHARED(6)→UNLOCKED
(II)對於一個寫事務完整經過:
第一階段:
語句序列:(1)——>(2)——>(6)
狀態變化:UNLOCKED→PENDING(1)→PENDING、SHARED(2)→SHARED(6)。此時事務獲得SHARED LOCK。
第二個階段:
語句序列:(3)
此時事務獲得RESERVED LOCK。
第三個階段:
事務執行修改操作。
第四個階段:
語句序列:(1)——>(4)——>(5)
狀態變化爲:
RESERVED→ RESERVED 、PENDING(1)→PENDING(4)→EXCLUSIVE(5)。此時事務獲得排斥鎖,就可以進行寫磁盤操作了。
注:在上面的過程中,由於(1)的執行,使得某些時刻SQLite處於兩種狀態,但它持續的時間很短,從某種程度上來說可以忽略,但是爲了把問題說清楚,在這裏描述了這一微妙而巧妙的過程。
4、SQLite的死鎖問題
SQLite的加鎖機制會不會出現死鎖?
這是一個很有意思的問題,對於任何採取加鎖作爲併發控制機制的DBMS都得考慮這個問題。有兩種方式處理死鎖問題:(1)死鎖預防(deadlock prevention)(2)死鎖檢測(deadlock detection)與死鎖恢復(deadlock recovery)。SQLite採取了第一種方式,如果一個事務不能獲取鎖,它會重試有限次(這個重試次數可以由應用程序運行預先設置,默認爲1次)——這實際上是基本鎖超時的機制。如果還是不能獲取鎖,SQLite返回SQLITE_BUSY錯誤給應用程序,應用程序此時應該中斷,之後再重試;或者中止當前事務。雖然基於鎖超時的機制簡單,容易實現,但是它的缺點也是明顯的——資源浪費。
5、事務類型(Transaction Types)
既然SQLite採取了這種機制,所以應用程序得處理SQLITE_BUSY 錯誤,先來看一個會產生SQLITE_BUSY錯誤的例子:
所以應用程序應該儘量避免產生死鎖,那麼應用程序如何做可以避免死鎖的產生呢?
答案就是爲你的程序選擇正確合適的事務類型。
SQLite有三種不同的事務類型,這不同於鎖的狀態。事務可以從DEFERRED,IMMEDIATE或者EXCLUSIVE,一個事務的類型在BEGIN命令中指定:
BEGIN [ DEFERRED | IMMEDIATE | EXCLUSIVE ] TRANSACTION;
一個deferred事務不獲取任何鎖,直到它需要鎖的時候,而且BEGIN語句本身也不會做什麼事情——它開始於UNLOCK狀態;默認情況下是這樣的。如果僅僅用BEGIN開始一個事務,那麼事務就是DEFERRED的,同時它不會獲取任何鎖,當對數據庫進行第一次讀操作時,它會獲取SHARED LOCK;同樣,當進行第一次寫操作時,它會獲取RESERVED LOCK。
由BEGIN開始的Immediate事務會試着獲取RESERVED LOCK。如果成功,BEGIN IMMEDIATE保證沒有別的連接可以寫數據庫。但是,別的連接可以對數據庫進行讀操作,但是RESERVED LOCK會阻止其它的連接BEGIN IMMEDIATE或者BEGIN EXCLUSIVE命令,SQLite會返回SQLITE_BUSY錯誤。這時你就可以對數據庫進行修改操作,但是你不能提交,當你COMMIT時,會返回SQLITE_BUSY錯誤,這意味着還有其它的讀事務沒有完成,得等它們執行完後才能提交事務。
Exclusive事務會試着獲取對數據庫的EXCLUSIVE鎖。這與IMMEDIATE類似,但是一旦成功,EXCLUSIVE事務保證沒有其它的連接,所以就可對數據庫進行讀寫操作了。
上面那個例子的問題在於兩個連接最終都想寫數據庫,但是他們都沒有放棄各自原來的鎖,最終,shared 鎖導致了問題的出現。如果兩個連接都以BEGIN IMMEDIATE開始事務,那麼死鎖就不會發生。在這種情況下,在同一時刻只能有一個連接進入BEGIN IMMEDIATE,其它的連接就得等待。BEGIN IMMEDIATE和BEGIN EXCLUSIVE通常被寫事務使用。就像同步機制一樣,它防止了死鎖的產生。
基本的準則是:如果你在使用的數據庫沒有其它的連接,用BEGIN就足夠了。但是,如果你使用的數據庫在其它的連接也要對數據庫進行寫操作,就得使用BEGIN IMMEDIATE或BEGIN EXCLUSIVE開始你的事務。