1. 程式人生 > >SQLite學習筆記(九)&&pager模塊

SQLite學習筆記(九)&&pager模塊

key 還原 data tag count image 持久化 生成 自身

概述

通過上一篇文章的分析,我們知道了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(least recent 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標記會被清除。

5.緩存一致性

從前面的討論知道,每個連接都有一份自己的緩存。對於共享緩存模式而言,同一個進程的多個線程公用一份緩存。那麽連接之間的緩存如何保證一致性?比如有Connection1和Connection2兩個連接,Connection1首先從文件中讀取了page_A並加入到了緩存;隨後Connection2也從文件中讀取Page_A,並進行了更新;那麽當Connection1再次讀取page_A時,Connection1如何知道自己緩存的page_A已經不是最新了,需要重新到DB文件中讀取?

SQLite當然考慮到這個問題,在SQLite DB的文件控制頭中存儲了DB的版本信息。每當會話執行SQL時,會首先加共享鎖,並讀取DB數據的版本信息(4個字節)並緩存起來,如果發現這次讀取的DB版本信息與之前緩存的DB數據版本信息有變(表示DB文件被修改了),則清理自身的緩存,清理緩存的接口是pager_reset。當會話再次請求已經發生變化的page時,會重新到DB文件中讀取,保證讀取到最新。至於DB的數據版本信息,每次事務提交時,會調用pager_write_changecounter進行更新,並持久化到DB文件頭。

由於這種實現方式,導致更新頻繁的系統,每次讀都會清空緩存,導致大量的請求實際還是需要請求DB文件。SQLite提供的共享緩存模式,使得多個會話可以共享一份緩存,就不存在所謂的清理緩存了,從這個層面來看,共享緩存除了可以節省內存空間,還可以提高緩存的命中率,尤其是對於多會話的場景。

日誌管理器

1.寫日誌策略

目前數據庫日誌主要用兩種方式:第一種是WAL(Write Ahead Logging),另外一種是影子分頁技術(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

https://www.cnblogs.com/cchust/p/4966790.html

SQLite學習筆記(九)&&pager模塊