SQLite數據庫學習小結——native層實現

1. SQlite概述

  SQLite是一款輕量、快速、跨平臺的嵌入式數據庫,是遵照ACID(注:ACID指數據庫事務正確執行的四個基本要素的縮寫。包含:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、持久性(Durability))的關係型數據庫管理系統,它包含在一個相對小的C庫中。html

  SQLite 的設計目標是簡單,從這種意義上說,SQLite 和其餘不少現代的 SQL 數據庫都不相同。SQLite 力求簡單,即便這致使了它的某些特徵偶爾執行效率比較低。它能夠很簡單地維護、定製、操做、管理和嵌入到 C 應用程序中。它使用簡單的技術實現了 ACID 特性。前端

  SQLite 把全部文件存儲在一個單一的普通本地文件裏面,你能夠把它放在本地系統的任何目錄中。任何有讀取該文件權限的用戶均可以讀取數據庫裏面的全部內容。任何一個有當前目錄寫入權限的用戶,均可以對數據庫作任何修改。SQLite 使用另外一個單獨的日誌文件來保存事務恢復信息,用來防止事務停止或系統故障。你能夠用程序指令來改變 SQLite 庫的一些行爲。sql

  SQLite 容許多個應用同時鏈接同一個數據庫。固然,它所支持的併發事務是有必定限制的:SQLite容許任何數量的併發讀取事務在數據庫上同時執行,但只容許一個寫入事務獨佔執行。    數據庫

  本文從native和Frameworks兩個層面的實現來講明Android系統中SQLite的實現。編程

2. SQlite的native層實現

1.1架構設計

  SQlite動態庫的軟件架構以下圖所示:後端

  SQLite的實現大致上能夠劃分爲前端和後端兩大塊。數組

前端:前端預處理應用程序輸入的 SQL 語句和 SQLite 命令。它分析、優化這些語句(和命令),而後生成後端能夠解析的 SQLite 內部字節碼程序。前端實現了 sqlite3_prepare 接口方法。緩存

(1)編程接口(Interface) cookie

  對數據庫使用者提供的接口方法。數據結構

(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。

  1. leaf:存放具體數據。
  2. internal:存放用於搜索的導航信息。
  3. overflow:存放leaf頁面存不下的數據。
  4. 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

The version-valid-for number.

96

4

SQLITE_VERSION_NUMBER

    注:考慮到跨平臺,數據庫文件的字節序是大頭(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自動建立事務稱爲原子事務,或者系統級事務

  建立系統級事務的代價是比較昂貴的,尤爲是對頻繁寫數據庫的場景,緣由是如下幾點:

  1. 每個寫事務都須要打開、寫入、關閉日誌文件。
  2. 執行完每一條SQL語句都要重建頁面緩存。
  3. 執行每一條事務都要申請文件鎖。

  提升讀寫效率的辦法是使用用戶級事務,即顯示的啓動一個事務,再執行多條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的變體,它的葉子節點只用來存放數據,它的中間節點只存放搜索信息和子節點指針。

  這兩種樹結構的中間節點能夠保存的子節點指針數量是可變的,且中間節點的數據遵循以下原則:

  1. 全部Ptr(0)指向的子樹節點的值都小於或等於Key(0)。
  2. 全部Ptr(1)指向的子樹節點的值都大於Key(0),但小於或等於Key(1)。
  3. 全部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》

2. https://www.sqlite.org

相關文章
相關標籤/搜索