標籤:
概述
通過上一篇文章的分析,我們知道了pager模組在整個sqlite中所處的位置。它是sqlite的核心模組,充當了多種重要角色。作為一個交易管理員,它通過並發控制和故障恢複實現事務的ACID特性,負責事務的原子提交和復原;作為一個頁管理器,它處理從檔案中讀寫資料頁,並執行檔案空間管理工作;作為日誌管理器,它負責寫日誌記錄到記錄檔;作為鎖管理器,它確保事務在訪問資料頁之前,一定先對資料檔案上鎖,實現並發控制。本質上來說,pager模組實現了儲存的持久性和事務的原子性。從圖1中我們可以看到pager模組主要由4個子模組組成:交易管理模組,鎖管理模組,日誌模組和緩衝模組。而事務模組的實現依賴於其它3個子模組。因此pager模組最核心的功能實質是由緩衝模組、日誌管理器和鎖管理器完成。Tree模組是pager模組的上遊,Tree模組在訪問資料檔案前,需要建立一個pager對象,通過pager對象來操作檔案。pager模組利用pager對象來追蹤檔案鎖相關的資訊,日誌狀態,資料庫狀態等。對於同一個檔案,一個進程可能有多個pager對象;這些對象之間都是相互獨立的。對於共用快取模式,每個資料檔案只有一個pager對象,所有串連共用這個pager對象。
圖1
緩衝模組
這裡談到的緩衝管理,實際上就是page cache模組。應用程式訪問資料庫檔案時,pager模組會以塊為單位進行緩衝,每一個串連都有自己專屬的pager模組,因此每個串連都有自己專屬的緩衝。
1.緩衝鎖狀態
檔案的頁面緩衝初始化時,pager模組處於NO_LOCK狀態。BTREE模組第一次調用sqlite3PagerGet從資料檔案中讀取頁面時,pager狀態轉換為SHARED_LOCK狀態。當Tree模組調用sqlite3PagerUnref釋放頁面時,pager狀態重新回到NO_LOCK狀態。當ree模組第一次調用sqlite3PagerWrite訪問頁面時,pager狀態變為RESERVED_LOCK狀態。要注意的是,調用sqlite3PagerWrite訪問的頁面,一定是之前被讀過的頁面,pager狀態是從SHARED_LOCK轉換到RESERVED_LOCK狀態。在向資料檔案頁寫入之前,pager模組轉換到EXECLUSIVE_LOCK狀態,事務提交sqlite3BtreeCommitPhaseTwo或復原sqlite3PagerRollback時,pager模組回到NO_LOCK狀態。
2.緩衝組織
2所示,緩衝其實是由一個hash表和多個鏈表組成。Pager模組訪問頁面時,首先以頁號為key,在hash表中尋找,迅速確定所需的頁面是否在緩衝。建立hash表時,若出現多個頁號映射在同一個桶,則通過鏈錶鏈接起來。除了hash表,緩衝組織還包括一個LRU鏈表和DIRTY鏈表,分別用於緩衝替換和緩衝刷髒。
圖2
3.緩衝策略
sqlite並不像有些資料庫有預取機制,可能是為了簡單,也可能是因為sqlite主要用於端裝置,本身緩衝就比較少。因此,只有在上層模組需要指定的頁時,才從檔案中讀取到緩衝中。由於緩衝是一定的,並且一般情況下小於資料庫檔案容量,所以一定會存在緩衝替換的問題。sqlite採用LRU演算法,基本上主流的關係型資料庫都採用這種演算法。LRU(leastrecent used),通過將過去一段時間頁面的訪問來預測未來頁面的訪問。意思就是,如果一個頁面現在被訪問,就可以認為這個頁面很有可能會被再次訪問;如果一個頁面在過去一段時間內很久都沒有被訪問,則可以認為該頁在未來一段時間內也不會被訪問。那麼當緩衝池滿時,則選擇最近最少被訪問的頁面替換。如果選擇被替換的頁是髒頁,在替換出緩衝之前,需要將先將被替換的頁寫入資料檔案(這時候髒頁對應的old-page需要先刷入記錄檔)。我們知道緩衝中的髒頁刷盤後才算真正寫入檔案,那麼緩衝中的髒頁何時刷盤?sqlite不提供刷髒頁介面,因此使用者不能主動觸發刷page(寫datafile)操作,這個操作由pager模組在特定情況下觸發。主要有兩種情況:緩衝的page數量已經超過了page_size;另外一種情況是,事務在提交過程中。
4.核心流程
讀取一個page的過程(假設頁號為P)
(1).在page cache中尋找
通過頁號,在hash表中搜尋,定位到指定的桶,然後通過PgHdr1.pNext逐個比較是否是需要的頁。如果找到,則將PgHdr.nRef加1,並將頁面返回給上層調用模組。
(2).如果在page cache中沒有找到,則擷取一個閒置slot,或者直接建立一個slot,只要不超過slot的閥值PCache1.nMax即可。
(3).如果沒有可用的slot,則選擇一個可以重用的slot(slot對應的頁面需要釋放,通過LRU演算法)
(4).如果選擇重用的slot對應的page是髒頁,則將該頁寫入檔案(對於wal,刷髒頁前,先將髒頁寫入記錄檔)
(5).載入page
如果頁號P對應的位移小於檔案的大小,從檔案讀入page到slot,設定PgHdr.nRef為1,返回;如果頁號P對應的位移大於當前檔案大小,則將slot中內容初始化為0,同樣將PgHdr.nRef設定為1。
更新page的過程
這裡假設page已經讀取到記憶體中。Tree模組往page寫入資料之前,需要調用sqlite3PagerWrite函數,使得一個page變為可寫的狀態,否則pager模組不知道這個page需要被修改。pager模組在資料檔案上加一個reserved鎖,並建立一個記錄檔。如果加鎖失敗,則返回SQLITE_BUSY錯誤。它將page的原始資訊拷貝到記錄檔,如果記錄檔中之前已經存在該page,則不進行拷貝動作,而只是將page標記為dirty。當page被寫入檔案後,dirty標記會被清除。
日誌管理器
1.寫日誌策略
目前資料庫日誌主要用兩種方式:第一種是WAL(Write AheadLogging),另外一種是影子分頁技術(Shadow paging)。sqlite分別實現了這兩種日誌方式來保證事務的ACID特性,可以通過參數journal_mode來控制記錄模式,預設情況下採用影子分頁技術。影子分頁模式下,每個page只在記錄檔中存一份,無論這個頁被修改過多少次。記錄檔中,只記錄事務開始前page的原始資訊,進行恢複時,只需要利用記錄檔中的page進行覆蓋即可。對於新產生的頁,日誌中不會記錄,而是在日誌頭記錄事務開始時資料檔案page的數目,進行恢複時,只需要截斷資料檔案即可,不需要新頁的資料,況且新頁本來就沒有任何資料。
2. Shadow paging
(1).將old-page寫入記錄檔,並fsync
(2).修改日誌頭,更新日誌記錄數,並fsync,這個值初始為0
(3).在資料檔案上擷取EXECLUSIVE lock,如果此時還有讀事務,則報SQLITE_BUSY錯誤
(4).將dirty-page寫入記錄檔,並將這些cache標記為clean,表示可以重用如果是因為cache滿了,導致需要寫datafile操作,由於使用者沒有發起事務提交,因此pager模組也不會提交事務。
pager模組重複上述的1,2,3,4點,直到事務提交。為了避免事務提交前,其他事務讀到髒資料,因此在進行刷髒頁時,需要在檔案上EXECLUSIVE lock,那麼直到事務提交前,這個EXECLUSIVE lock都不會釋放,導致這種情況下,所有其它讀、寫事務都被堵塞。因此,大事務會降低整體的並發效能。
鎖管理器
sqlite的並發控制靠封鎖實現,依據兩階段鎖協議,保證事務的ACID。Sqlite的並發控制依賴於檔案鎖,通過鎖檔案的特定的地區,實現互斥。Sqlite主要包含4種鎖,SHARED_LOCK(共用鎖定),(RESERVED_LOCK)保留鎖,(PENDING_LOCK)未決鎖和(EXCLUSIVE_LOCK)排它鎖,其中共用鎖定與排它鎖在檔案的同一片地區。RESERVED_LOCK主要用於寫寫互斥,PENDING_LOCK則主要用於讀寫互斥,並有延緩互斥的作用。關於鎖並發詳細可以看sqlite封鎖機制。
圖3
關鍵介面
sqlite3PagerCommitPhaseOne //提交事務第一階段:檔案修改計數器增1,將記錄檔刷入磁碟,將事務修改的髒頁刷入磁碟。
sqlite3PagerCommitPhaseTwo //提交事務的第二階段:刪除記錄檔,釋放鎖
sqlite3PagerRollback //復原事務:復原事務在資料檔案的修改,將排它鎖降級為共用鎖定,所有快取頁面面還原到修改之前的狀態,刪除記錄檔。
pcache1ResizeHash【擴充hash】 //最大緩衝2000個page,LRU鏈表,插入隊頭,從隊尾刪除
setSharedCacheTableLock //table-lock介面
pagerLockDb //檔案鎖介面
walLockShared wal //檔案分享權限設定讀鎖介面
walLockExclusive wal //檔案排它鎖介面
sqlite3PagerAcquire //擷取某一頁
readDbPage //讀取page
pcache1FetchNoMutex //尋找cache
pcache1FetchStage2 //添加cache
pcache1RemoveFromHash //移除cache
參考文檔
SQlite Database System Design and Implementation
SQLite學習筆記(九):pager模組