1. SQlite概述
SQLite是一款輕量、快速、跨平臺的嵌入式資料庫,是遵守ACID(注:ACID指資料庫事務正確執行的四個基本要素的縮寫。包含:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、永續性(Durability))的關係型資料庫管理系統,它包含在一個相對小的C庫中。
SQLite 的設計目標是簡單,從這種意義上說,SQLite 和其他很多現代的 SQL 資料庫都不相同。SQLite 力求簡單,即使這導致了它的某些特徵偶爾執行效率比較低。它可以很簡單地維護、定製、操作、管理和嵌入到 C 應用程式中。它使用簡單的技術實現了 ACID 特性。
SQLite 把所有檔案儲存在一個單一的普通本地檔案裡面,你可以把它放在本地系統的任何目錄中。任何有讀取該檔案許可權的使用者都可以讀取資料庫裡面的所有內容。任何一個有當前目錄寫入許可權的使用者,都可以對資料庫做任何修改。SQLite 使用另一個單獨的日誌檔案來儲存事務恢復資訊,用來防止事務中止或系統故障。你可以用程式指令來改變 SQLite 庫的一些行為。
SQLite 允許多個應用同時連線同一個資料庫。當然,它所支援的併發事務是有一定限制的:SQLite允許任何數量的併發讀取事務在資料庫上同時執行,但只允許一個寫入事務獨佔執行。
本文從native和Frameworks兩個層面的實現來說明Android系統中SQLite的實現。
2. SQlite的native層實現
1.1架構設計
SQlite動態庫的軟體架構如下圖所示:
SQLite的實現大體上可以劃分為前端和後端兩大塊。
前端:前端預處理應用程式輸入的 SQL 語句和 SQLite 命令。它分析、優化這些語句(和命令),然後生成後端可以解析的 SQLite 內部位元組碼程式。前端實現了 sqlite3_prepare 介面方法。
(1)程式設計介面(Interface)
對資料庫使用者提供的介面方法。
(2)詞法分析器(The tokenizer)
分詞器把輸入的語句分割成記號。
(3)語法分析器(The parser)
詞法分析器通過分析詞法分析器產生的記號來確定語句的結構,並生成解析樹。語法分析器還包括一個優化器來重構解析樹,並生成一個可以產生高效率的位元組碼(bytecode)程式的等效的解析樹。
(4)程式碼生成器(The code generator)
程式碼生成器遍歷解析樹,生成一個等效的位元組碼程式。
後端:後端是一個解釋位元組碼(bytecode)程式的引擎。後端做實際的資料庫讀寫工作。後端實現了 sqlite3_bind_*,sqlite3_step,sqlite3_column_*,sqlite3_reset 和 sqlite3_finalize 介面方法。
(1)虛擬機器 (VM, The Virtual Machine)
虛擬機器是內部位元組碼(bytecode)語言的解釋程式。它執行位元組碼(bytecode)程式來執行 SQL 語句的操作。它是資料庫資料的最終操作者。它把資料庫看作表和索引的集合,而表或索引則是一系列資料記錄的集合。
(2)B/B+-tree
B/B+ 樹把每一個資料記錄組織到一個有序的樹形資料結構中;表和索引分別存放在 B+ 樹和 B 樹。此模組幫助 VM 在樹中搜尋、插入和刪除資料記錄。它也幫助 VM 建立新的樹,刪除舊的樹。
(3)頁面管理器(pager)
Pager 模組在檔案系統頂部實現了面向頁面的資料庫檔案抽象。它管理記憶體中的由 B/B+ 樹使用的資料庫頁面快取,另外,它也管理檔案鎖和日誌記錄,以便實現事務的 ACID 特性。
(4)作業系統介面
作業系統介面模組為不同的作業系統抽象出了統一的介面。
1.2各模組實現
本節介紹SQLite各模組的實現方式。
1.2.1資料庫檔案格式
SQLite把整個資料庫儲存在一個資料庫檔案中。
為了便於管理和讀寫資料庫,SQLite 把每一個資料庫(包括記憶體資料庫(in-memory database))都劃分為大小固定的頁面(page),頁面大小可以是512~32768位元組(預設1024位元組,在資料庫檔案建立之前可以通過pragma命令修改頁大小)。資料庫檔案可以看作是頁面的陣列,頁面的索引是從1開始,不是從0開始(頁面0會被當作NULL頁面,即不是一個實際存在的頁面),頁面索引最大是2^31-1。
頁面的型別可以分為4種:leaf、internal、overflow和free。
-
leaf:存放具體資料。
-
internal:存放用於搜尋的導航資訊。
-
overflow:存放leaf頁面存不下的資料。
-
free:頁面是空閒的,等待分配使用。
第1頁一定是internal型別的,而且第1頁的前100位元組固定存放檔案頭資訊,它描述了整個資料庫的屬性。檔案頭的格式如下:
Offset |
Size |
Description |
0 |
16 |
The header string: "SQLite format 3\000" |
16 |
2 |
The database page size in bytes. Must be a power of two between 512 and 32768 inclusive, or the value 1 representing a page size of 65536. |
18 |
1 |
File format write version. 1 for legacy; 2 for WAL. |
19 |
1 |
File format read version. 1 for legacy; 2 for WAL. |
20 |
1 |
Bytes of unused "reserved" space at the end of each page. Usually 0. |
21 |
1 |
Maximum embedded payload fraction. Must be 64. |
22 |
1 |
Minimum embedded payload fraction. Must be 32. |
23 |
1 |
Leaf payload fraction. Must be 32. |
24 |
4 |
File change counter. |
28 |
4 |
Size of the database file in pages. The "in-header database size". |
32 |
4 |
Page number of the first freelist trunk page. |
36 |
4 |
Total number of freelist pages. |
40 |
4 |
The schema cookie. |
44 |
4 |
The schema format number. Supported schema formats are 1, 2, 3, and 4. |
48 |
4 |
Default page cache size. |
52 |
4 |
The page number of the largest root b-tree page when in auto-vacuum or incremental-vacuum modes, or zero otherwise. |
56 |
4 |
The database text encoding. A value of 1 means UTF-8. A value of 2 means UTF-16le. A value of 3 means UTF-16be. |
60 |
4 |
The "user version" as read and set by the user_version pragma. |
64 |
4 |
True (non-zero) for incremental-vacuum mode. False (zero) otherwise. |
68 |
4 |
The "Application ID" set by PRAGMA application_id. |
72 |
20 |
Reserved for expansion. Must be zero. |
92 |
4 |
|
96 |
4 |
注:考慮到跨平臺,資料庫檔案的位元組序是大頭(big-endian)的。
SQLite資料庫檔案格式詳情參見https://www.sqlite.org/fileformat2.html。
1.2.2作業系統介面(OS Interface)
作業系統介面抽象層是SQLite實現跨平臺的關鍵模組,也叫虛擬檔案系統(VFS),它對上層提供操作檔案系統的統一介面方法,底層呼叫對應作業系統的介面方法實現具體的檔案操作。
1.2.3頁面管理器(Pager)
Pager是唯一訪問底層資料庫檔案和日誌(journal)檔案的模組。它本身既不解析,也不修改頁面內容,而是把資料庫檔案抽象成了基於頁面的可隨機訪問的快取。上層模組(B+-tree)總是使用Pager的提供介面方法讀寫資料庫檔案,而不會直接訪問資料庫檔案或日誌檔案。
Pager的主要職責是管理頁面快取,負責從資料庫讀入需要的頁面(為了減少記憶體使用,頁面只在需要時讀入)。此外,為了支援對上層透明的資料庫讀寫操作,Pager還封裝了其他職責,包括:事務管理、資料管理、日誌管理和檔案鎖管理。總的來說,Pager保證了資料庫儲存的永續性和事務操作的原子性。如下:
在上層模組(B+-tree)看來,資料庫操作就是一系列的事務的執行,而不需要關心事務的事務執行是如何實現的,Pager會把事務執行細分為獲取檔案鎖、記錄日誌、讀寫資料庫檔案等操作,下面對Pager的這幾個模組的實現分別簡要介紹一下。
1.2.3.1 頁面快取管理
當SQLite開啟一個資料庫,就會建立一個Pager來管理頁面快取,如果同一個執行緒開啟多次資料庫,只會建立一個頁面快取,如果多個執行緒開啟同一個資料庫,則每個執行緒會建立一個頁面快取。
Pager通過一個雜湊表來管理頁面快取,雜湊表開始時是空的,隨著資料庫的訪問Pager會不斷往裡面插入新的頁面,頁面快取的結構如下:
Pager管理頁面快取的原則是"按需讀取"和"延遲寫入"。
頁面快取中快取的頁面數量是有限的,當從資料庫檔案讀取資料到頁面快取而沒有空閒頁面時,Pager會根據LRU策略回收一個頁面(如果頁面有髒資料,會先寫入資料庫檔案),然後再讀取資料到空閒出來的頁面中。
1.2.3.2 事務管理
事務管理是保證資料庫同步的關鍵,SQLite依賴檔案鎖和日誌來實現資料庫事務的ACID屬性,SQLite資料庫只支援序列事務,不支援事務的巢狀和儲存點(savepoint)。
SQLite執行的每一條SQL語句都必須放在事務中,讀取資料操作放在讀寫事務(read-or-write-transaction)中,寫入資料操作放在寫事務(write-transaction)中。應用程式並不需要為每一條SQL語句啟動一個事務,SQLite會為自動每一條獨立執行的SQL語句建立對應的事務並執行,這種由SQLite自動建立事務稱為原子事務,或者系統級事務。
建立系統級事務的代價是比較昂貴的,尤其是對頻繁寫資料庫的場景,原因是以下幾點:
-
每一個寫事務都需要開啟、寫入、關閉日誌檔案。
-
執行完每一條SQL語句都要重建頁面快取。
-
執行每一條事務都要申請檔案鎖。
提高讀寫效率的辦法是使用使用者級事務,即顯示的啟動一個事務,再執行多條SQL語句,最後提交事務。
SQLite只支援序列的事務,所以應用程式在一個資料庫連線上只能執行一個事務,在事務內部再次啟動事務會得到一個錯誤。
1.2.3.3 日誌管理
日誌是一個資訊庫,用來在事務放棄(abort)、應用程式異常或者系統異常時恢復資料庫資料,它只能用來回滾事務操作。SQLite為每個資料庫檔案使用一個日誌檔案,檔名字首和資料庫檔名相同並以-journal結尾。
SQLite同一時刻只允許一個寫事務執行,它會在執行時建立日誌檔案,在提交後刪除日誌檔案。在事務修改一個頁面之前,SQLite會把正在被修改的頁面寫入日誌檔案,以備將來回復使用,日誌記錄的格式如下:
1.2.3.4 檔案鎖管理
SQLite使用鎖機制來保證事務的序列執行,它在事務開始執行時獲取鎖,在事務執行完畢或者放棄後釋放鎖。SQLite使用的是資料庫級別的鎖,而不是行、表或頁面級別的鎖。
在事務看來,資料庫檔案有五種鎖狀態:NOLOCK(未鎖定),SHARED(共享只讀),RESERVED(正在讀取,即將寫入,可以獲取SHARED鎖,只能有一個),PENDING(等待寫入,禁止獲取SHARED鎖,只能有一個),EXLUSIVE(只能寫入,禁止獲取SHARED鎖,只能有一個)。
各種鎖狀態的轉換時序如下:
如果有多個執行緒同時申請RESERVED鎖(它們已經持有SHARED鎖),得到RESERVED鎖的執行緒會等待其他執行緒釋放SHARED鎖才能繼續獲取EXLUSIVE鎖,此時另一個執行緒如果等待獲取RESERVED鎖可能會引起死鎖。SQLite為避免這種死鎖,使用的方式是非阻塞方式獲取鎖,即另一個執行緒嘗試獲取幾次RESERVED鎖,如果獲取不到就會放棄,並返回SQLITE_BUSY錯誤。
鎖的具體實現依賴作業系統提供的檔案鎖介面。
1.2.4表和索引管理(B/B+-tree)
在Pager提供的面向頁面的資料快取基礎上,SQLite使用B+-tree來組織所有資料記錄,每一張表對應一個B+-tree。而資料庫表的索引則被存放在B-tree中。
B-tree也叫多路搜尋樹,B-tree的B代表是的是"balanced",balanced的意思就是所有葉子節點都在同一層次。B-tree的所有搜尋資訊和資料都可以儲存在中間節點和葉子節點,它一般用於組織索引資訊,使用B-tree結構可以顯著減少定位記錄時所經歷的中間過程,從而加快查詢速度。
B+-tree是B-tree的變體,它的葉子節點只用來存放資料,它的中間節點只存放搜尋資訊和子節點指標。
這兩種樹結構的中間節點可以儲存的子節點指標數量是可變的,且中間節點的資料遵循如下原則:
-
所有Ptr(0)指向的子樹節點的值都小於或等於Key(0)。
-
所有Ptr(1)指向的子樹節點的值都大於Key(0),但小於或等於Key(1)。
-
所有Ptr(n)指向的子樹節點的值都大於Key(n-1)。
B+-tree中間節點的資料結構如下:
SQLite中的B+-tree是通過分配根節點來建立的,根節點不能重複分配,每個B+-tree都是通過其根節點的頁面編號來標識的。頁面編號存放在主目錄表中,主目錄表(master catalog table)的根節點存放在資料庫檔案的第1個頁面中。
B+-tree的結構如下:
SQLite把各樹節點存(包括中間節點和葉子節點)放在不同的頁面中,一個頁面存放一個節點。每個節點存放資料負載(payload)的空間大小是固定的,如果資料負載超出了節點的大小,那麼超出的部分會存放到附加頁面(overflow page)中,中間節點和葉子節點都可以有附加頁面存放超出的資料負載。頁面的結構如下:
頁面的header結構定義如下:
每個頁面都被劃分為若干個資料單元(cell),每個資料單元(cell)儲存一份資料負載(或資料負載的一部分)。資料單元的header定義如下:
資料單元(cell)是大小可變的位元組陣列,一個資料單元(cell)儲存一份資料負載(payload)。
每個頁面可以儲存的資料單元數量不能少於最低單元數量,也不能多餘最大單元數量,它們分別由資料庫檔案頭中的"Maximum embedded payload fraction"和"Minimum embedded payload fraction"決定,資料負載超出的部分會被放到附加頁面中,多個附加頁面會通過連結串列組織起來。
總結,SQLite會把資料庫中的表和索引通過B/B+-tree組織起來,B/B+tree會在插入或刪除節點時做自動調整以保持平衡,並且會自動回收和重用空閒頁面。B/B+-tree提供的查詢、插入和刪除操作的時間複雜度是O(logn),遍歷記錄的時間複雜度是O(1)。
1.2.5虛擬機器(VM)
SQLite後端的頂層元件是虛擬機器,它是前端和後端的介面,虛擬機器在本地系統的的上層又抽象出了一個虛擬的機器。虛擬機器執行的是SQLite內部定義的位元組碼(bytecode)程式語言,這個程式語言是為執行資料庫搜尋、讀取和修改特別設計的。虛擬機器接收和執行位元組碼程式,再通過B+-tree具體執行資料庫操作並返回操作結果。
一個位元組碼(bytecode)程式是由sqlite3_stmt型別的物件封裝的,由前端的命令解析器在解析SQL命令後生成。
參考資料:
1.《Inside SQLite》