SQLite學習筆記(六)&&共享緩存


介紹

       通常情況下,sqlite中每個連接都會一個獨立的pager對象,pager對象中管理了該連接的緩存信息,通過pragma cache_size指令可以設置緩存大小,默認是2000個page,每個page是1024B。這樣導致了對於同一個數據文件,多個連接各自維護了自己的一份緩存,在高並發情況下,可能導致使用大量的內存。而sqlite作為一個嵌入式數據庫,通常用於嵌入式設備,內存可能比較有限,為了應對這種問題,sqlite提供了一種方法,通過讓多個連接公用一個pager對象,共享同一份緩存。

      當打開這個特性后,多個連接可以共享一個pager對象,這樣在一定程度上減少了內存的消耗和文件的IO次數。一個線程的多個連接對象,以及多個線程的多個連接對象都可以采用這種方式來共享緩存。由於sqlite以動態庫方式嵌入在應用程序中,每個應用程序有自己獨立的進程空間,因此該特性不能用於進程之間,同一個進程可以共享一份緩存,進程之間各自維護自己的緩存。關於緩存的實現后面會單獨寫一篇文章。下圖展示了啟用共享緩存后的結構圖。


圖1

從圖1中可以看到,Process1中的兩個連接共享BtShared對象,BtShared對象對應一個Pager對象,而緩存由Pager對象管理,因此整個Process1中的所有連接公用一個緩存,通過Pager模塊操作Page,IO操作對上層模塊透明。

實現原理

      調用接口sqlite3_open_v2打開連接時,通過指定參數SQLITE_OPEN_SHAREDCACHE來聲明連接采用共享緩存模式。之前SQLite系列(二):常規性能測試 中的單表主鍵查詢測試章節中提到,開啟共享緩存模式下,導致應用的程序的並發性能大大下降,多線程情況下,CPU也只能用一個核。因此大家在實際使用中,要權衡內存和並行度,來確定是否開啟共享緩存模式。
      我們的測試場景都是只讀,理論上不應該存在並發沖突,那么為什么不能並行?這里要看共享緩存的實現了。從圖1中也可以看到,每個連接有一個btree對象,多個btree對象通過共享BtShared對象來共享緩存,BtShared對象通過mutux來維護里面的成員,包括page cache的管理,table-lock信息等,因此這個mutex是一個熱點,在高並發場景下,多個線程同時訪問BtShared對象,會由於競爭mutex,無法充分並發,導致並行度差。

table-lock

      默認情況下,sqlite只通過文件鎖就可以實現讀寫互斥,讀讀並發的效果,關於這點我在SQLite系列(五):SQLITE封鎖機制中已經說明。那么為什么要引入table-lock?這個也是拜共享緩存所賜。共享緩存模式下,多個btree對象對應同一個BtShared對象,在執行更新時,首先會修改pager中cache,此時更新事務加了reserved-lock,與讀事務的shared-lock不互斥。為了避免讀到臟頁,在共享緩存模式下,增加了table-lock,避免讀寫事務同時訪問同一個cache,導致臟讀的情況發生。用戶訪問具體某個表的page之前,會首先調用sqlite3BtreeLockTable對該表設置一個READ-LOCK(select 操作)或WRITE-LOCK(DML操作),如果發現沖突,則拋出SQLITE_LOCKED錯誤。通過這種方式,保證了多個線程不會同時對一個表進行讀寫操作。函數sqlite3BtreeLockTable部分實現如下:

sqlite3BtreeEnter(p);

//判斷加鎖是否沖突
rc = querySharedCacheTableLock(p, iTab, lockType);
if( rc==SQLITE_OK ){
  //加鎖
  rc = setSharedCacheTableLock(p, iTab, lockType);
}
  
sqlite3BtreeLeave(p); 

從上面的代碼可以看到,若加鎖沖突,則直接報SQLITE_LOCKED錯誤;否則加上鎖。注意看到判斷加鎖和加鎖過程通過BtShared對象的mutex來保護(sqlite3BtreeEnter,sqlite3BtreeLeave),因此加鎖過程是串行的,table-lock鏈表不會被多個線程同時操作。

querySharedCacheTableLock代碼邏輯

/* If some other connection is holding an exclusive lock, the

** requested lock may not be obtained.

*/

if( pBt->pWriter!=p && (pBt->btsFlags & BTS_EXCLUSIVE)!=0 ){
  
     sqlite3ConnectionBlocked(p->db, pBt->pWriter->db);
     return SQLITE_LOCKED_SHAREDCACHE;
}
setSharedCacheTableLock邏輯
for(pIter=pBt->pLock; pIter; pIter=pIter->pNext){
    if( pIter->iTable==iTable && pIter->pBtree==p ){   
    pLock = pIter;
    break;
  }
}

/*create a table lock*/
if( !pLock ){
   pLock = (BtLock *)sqlite3MallocZero(sizeof(BtLock));

   if( !pLock ){
    
   return SQLITE_NOMEM;
  }
  
pLock->iTable = iTable;
pLock->pBtree = p;
pLock->pNext = pBt->pLock;
pBt->pLock = pLock;
}

遍歷BtShared對象中已有鎖鏈表,比較iTable和對應的pBtree是否與自身相同(自己是否已經加過),若沒有,則申請鎖對象,加入鏈表。

加鎖流程

我們知道sqlite有兩種日志模式,默認的DELETE模式和WAL模式,下面我會介紹開啟共享緩存模式后,更新操作的加鎖流程,主要變化在於table-lock。

普通日志模式+共享緩存模式

  1. 開啟事務:shared-lock[sqlite3BtreeBeginTrans]
  2. DML操作:
  • 文件鎖,reserved-lock
  • table-lock
    將對應的表加鎖,同一個表的讀鎖與寫鎖互斥
  • 讀取表對應的page
  1. 提交:
  • 加execlusive-lock [sqlite3PagerCommitPhaseOne,刷日志]
  • 刪除日志文件
  • 釋放execlusive-lock
  • 釋放table-lock

 WAL日志模式+共享緩存模式

  1. 開啟事務,shared-lock[sqlite3BtreeBeginTrans]
  2. DML操作
  • 數據文件鎖,shared-lock
  • wal日志文件write-lock
  • table-lock
  1. 提交
  • 釋放table-lock
  • 釋放日志文件write-lock
  • 釋放數據文件shared-lock


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM