標籤:
介紹
通常情況下,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。
普通記錄模式+共用快取模式
- 開啟事務:shared-lock[sqlite3BtreeBeginTrans]
- DML操作:
- 檔案鎖,reserved-lock
- table-lock,
將對應的表加鎖,同一個表的讀鎖與寫鎖互斥
- 讀取表對應的page
- 提交:
- 加execlusive-lock [sqlite3PagerCommitPhaseOne,刷日誌]
- 刪除記錄檔
- 釋放execlusive-lock
- 釋放table-lock
WAL記錄模式+共用快取模式
- 開啟事務,shared-lock[sqlite3BtreeBeginTrans]
- DML操作
- 資料檔案鎖,shared-lock
- wal記錄檔write-lock
- table-lock
- 提交
- 釋放table-lock
- 釋放記錄檔write-lock
- 釋放資料檔案shared-lock
SQLite學習筆記(六)&&共用快取