SQLite入門與分析(五)---Page Cache之併發控制

寫在前面:本節主要談談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開始你的事務。


發佈了6 篇原創文章 · 獲贊 3 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章