一步一步了解WCDB

你好,WCDB

WCDB是一個高效、完整、易用的移動數(shù)據(jù)庫框架,基于SQLCipher,支持iOS, macOS和Android。

1 基本特性

  • 易用,WCDB支持一句代碼即可將數(shù)據(jù)取出并組合為object。

  • WINQ(WCDB語言集成查詢):通過WINQ,開發(fā)者無須為了拼接SQL的字符串而寫一大坨膠水代碼。

  • ORM(Object Relational Mapping):WCDB支持靈活、易用的ORM。開發(fā)者可以很便捷地定義表、索引、約束,并進行增刪改查操作。

[database getObjectsOfClass:WCTSampleConvenient.class
                    fromTable:tableName
                        where:WCTSampleConvenient.intValue>=10
                        limit:20];                      
  • 高效,WCDB通過框架層和sqlcipher源碼優(yōu)化,使其更高效的表現(xiàn)。

  • 多線程高并發(fā):WCDB支持多線程讀與讀、讀與寫并發(fā)執(zhí)行,寫與寫串行執(zhí)行。

  • 批量寫操作性能測試


    更多關于WCDB的性能數(shù)據(jù),請參考benchmark。

  • 完整,WCDB覆蓋了數(shù)據(jù)庫相關各種場景的所需功能。

  • 加密:WCDB提供基于SQLCipher的數(shù)據(jù)庫加密。

  • 損壞修復:WCDB內建了Repair Kit用于修復損壞的數(shù)據(jù)庫。

  • 反注入:WCDB內建了對SQL注入的保護。

2 數(shù)據(jù)庫修復方案

通過收集到的大量案例和日志,分析出實際上移動端數(shù)據(jù)庫損壞的真正原因其實就3個:

  • 空間不足
  • 設備斷電
  • 文件 sync 失敗

我們需要針對這些原因一一進行優(yōu)化

2.1 優(yōu)化空間占用

  • 業(yè)務文件先申請后使用,如果某個文件沒有申請就使用了,會被自動掃描出來并刪除;
  • 每個業(yè)務文件都要申明有效期,是一天、一個星期、一個月還是永久存儲;
  • 過期文件會被自動清理。

對于微信之外的空間占用,例如相冊、視頻、其他App的空間占用,微信本身是做不了什么事情的,我們可以提示用戶進行空間清理

2.2 優(yōu)化文件 sync

2.2.1 synchronous = FULL

設置SQLite的文件同步機制為全同步,亦即要求每個事物的寫操作是真的flush到文件里去。

2.2.2 fullfsync = 1

通過與蘋果工程師的交流,我們發(fā)現(xiàn)在 iOS 平臺下還有 fullfsync 這個選項,可以嚴格保證寫入順序跟提交順序一致。設備開發(fā)商為了測評數(shù)據(jù)好看,往往會對提交的數(shù)據(jù)進行重排,再統(tǒng)一寫入,亦即寫入順序跟App提交的順序不一致。在某些情況下,例如斷電,就可能導致寫入文件不一致的情況,導致文件損壞。

2.3 SQLite 修復邏輯優(yōu)化

官方修復算法是這樣一個流程:從 master 表中讀出一個個表的信息,根據(jù)根節(jié)點地址和創(chuàng)表語句來 select 出表里的數(shù)據(jù),能 select 多少是多少,然后插入到一個新 DB 中。要注意的是 master 表他本身也是一個 B+樹 形式的普通表,DB 第0頁就是他的根節(jié)點。那么只要 master 表某個節(jié)點損壞,這個節(jié)點下面記錄的表就都恢復不了。更壞的情況是 DB 第0頁損壞,那么整個 master 表都讀不出來,就導致整個DB都恢復失敗。這就是官方修復算法成功率這么低的原因,太依賴 master 表了。

2.3.1 解析B-tree恢復方案(RepairKit)

正常情況下,SQLite 引擎打開DB后首次使用,需要先遍歷sqlite_master,并將里面保存的SQL語句再解析一遍, 保存在內存中供后續(xù)編譯SQL語句時使用。假如sqlite_master損壞了無法解析,“Dump恢復”這種走正常SQLite 流程的方法,自然會卡在第一步了。為了讓sqlite_master受損的DB也能打開,需要想辦法繞過SQLite引擎的邏輯。 由于SQLite引擎初始化邏輯比較復雜,為了避免副作用,沒有采用hack的方式復用其邏輯,而是決定仿造一個只可以 讀取數(shù)據(jù)的最小化系統(tǒng)

sqlite_master信息量比較小,而且只有改變了表結構的時候(例如執(zhí)行了CREATE TABLE、ALTER TABLE等語句)才會改變,因此對它進行備份成本是非常低的,一般手機典型只需要幾毫秒到數(shù)十毫秒即可完成,一致性也容易保證, 只需要執(zhí)行了上述語句的時候重新備份一次即可。有了備份,我們的邏輯可以在讀取DB自帶的sqlite_master失敗的時候 使用備份的信息來代替。

DB初始化的問題除了文件頭和sqlite_master完整性外,還有加密。SQLCipher加密數(shù)據(jù)庫,對應的恢復邏輯還需要加上 解密邏輯。按照SQLCipher的實現(xiàn),加密DB 是按page 進行包括頭部的完整加密,所用的密鑰是根據(jù)用戶輸入的原始密碼和 創(chuàng)建DB 時隨機生成的 salt 運算后得出的??梢圆孪氲玫剑绻4鎠alt錯了,將沒有辦法得出之前加密用的密鑰, 導致所有page都無法讀出了。由于salt 是創(chuàng)建DB時隨機生成,后續(xù)不再修改,將它納入到備份的范圍內即可

到此,初始化必須的數(shù)據(jù)就保證了,可以仿造讀取邏輯了。我們常規(guī)使用的讀取DB的方法(包括dump方式恢復), 都是通過執(zhí)行SQL語句實現(xiàn)的,這牽涉到SQLite系統(tǒng)最復雜的子系統(tǒng)——SQL執(zhí)行引擎。我們的恢復任務只需要遍歷B-tree所有節(jié)點, 讀出數(shù)據(jù)即可完成,不需要復雜的查詢邏輯,因此最復雜的SQL引擎可以省略。同時,因為我們的系統(tǒng)是只讀的, 寫入恢復數(shù)據(jù)到新 DB 只要直接調用 SQLite 接口即可,因而可以省略同樣比較復雜的B-tree平衡、Journal和同步等邏輯。 最后恢復用的最小系統(tǒng)只需要:

  • VFS讀取部分的接口(Open/Read/Close),或者直接用stdio的fopen/fread、Posix的open/read也可以

  • SQLCipher的解密邏輯

  • B-tree解析邏輯

即可實現(xiàn)

B-tree解析好處是準備成本較低,不需要經(jīng)常更新備份,對大部分表比較少的應用備份開銷也小到幾乎可以忽略, 成功恢復后能還原損壞時最新的數(shù)據(jù),不受備份時限影響。 壞處是,和Dump一樣,如果損壞到表的中間部分,比如非葉子節(jié)點,將導致后續(xù)數(shù)據(jù)無法讀出。

使用 Repair Kit 可以直接從損壞的數(shù)據(jù)庫里盡量讀出未損壞的數(shù)據(jù),不需要事先準備, 但是先備份 Master 信息可以大大增加恢復成功率。 如果有意使用 Repair Kit 恢復數(shù)據(jù)庫, 建議備份 Master 信息

2.3.2 備份方案

主要的方案有:

  • 拷貝: 不能再直白的方式。由于SQLite DB本身是文件(主DB + journal 或 WAL), 直接把文件復制就能達到備份的目的。

  • Dump: 上一個恢復方案用到的命令的本來目的。在DB完好的時候執(zhí)行.dump, 把 DB所有內容輸出為 SQL語句,達到備份目的,恢復的時候執(zhí)行SQL即可。

  • Backup API: SQLite自身提供的一套備份機制,按 Page 為單位復制到新 DB, 支持熱備份。

對以上方案做簡單測試后,備份方案也就基本定下了。測試用的DB大小約 50MB, 數(shù)據(jù)條目數(shù)大約為 10萬條:

微信在Dump + gzip方案上再加以優(yōu)化,由于格式化SQL語句輸出耗時較長,因此使用了自定義 的二進制格式承載Dump輸出。第二耗時的壓縮操作則放到別的線程同時進行,在雙核以上的環(huán)境 基本可以做到無額外時間消耗。由于數(shù)據(jù)保密需要,二進制Dump數(shù)據(jù)也做了加密處理。 采用自定義二進制格式還有一個好處是,恢復的時候不需要重復的編譯SQL語句,編譯一次就可以 插入整個表的數(shù)據(jù)了,恢復性能也有一定提升。優(yōu)化后的方案比原始的Dump + 壓縮, 每秒備份行數(shù)提升了 150%,每秒恢復行數(shù)也提升了 40%。

2.3.3 不同方案的組合

由于解析B-tree恢復原理和備份恢復不同,失敗場景也有差別,可以兩種手段混合使用覆蓋更多損壞場景。 微信的數(shù)據(jù)庫中,有部分數(shù)據(jù)是臨時或者可從服務端拉取的,這部分數(shù)據(jù)可以選擇不修復,有些數(shù)據(jù)是不可恢復或者 恢復成本高的,就需要修復了。

如果修復過程一路都是成功的,那無疑使用B-tree解析修復效果要好于備份恢復。備份恢復由于存在 時效性,總有部分最新的記錄會丟掉,解析修復由于直接基于損壞DB來操作,不存在時效性問題。 假如損壞部分位于不需要修復的部分,解析修復有可能不發(fā)生任何錯誤而完成。

若修復過程遇到錯誤,則很可能是需要修復的B-tree損壞了,這會導致需要修復的表發(fā)生部分或全部缺失。 這個時候再使用備份修復,能挽救一些缺失的部分。

最早的Dump修復,場景已經(jīng)基本被B-tree解析修復覆蓋了,若B-tree修復不成功,Dump恢復也很有可能不會成功。 即便如此,假如上面的所有嘗試都失敗,最后還是會嘗試Dump恢復。

注:了解到iOS端恢復方式只提供Repair Kit, 且所有的備份和恢復操作都需要開發(fā)人員自己調用相應的接口

3 SQLite源文件優(yōu)化

3.1 優(yōu)化并發(fā)效率

3.1.1 SQLite 多句柄方案

我們先講 SQLite 所提供的多線程并發(fā)方案。它對這方面的支持做的很不錯,在使用上,只需

  • 1.開啟句柄多線程支持的配置 PRAGMA SQLITE_THREADSAFE=2
  • 2.確保同一個句柄同一時間只有一個線程在操作
  • 3.(可選)開啟 WAL 模式 PRAGMA journal_mode=WAL

此時寫操作會先 append 到 wal 文件末尾,而不是直接覆蓋舊數(shù)據(jù)。而讀操作開始時,會記下當前的 WAL 文件狀態(tài),并且只訪問在此之前的數(shù)據(jù)。這就確保了多線程讀與讀、讀與寫之間可以并發(fā)地進行。

3.1.2 Busy Retry 方案

而寫與寫之間仍會互相阻塞。SQLite 提供了 Busy Retry 的方案,即發(fā)生阻塞時,會觸發(fā) Busy Handler,此時可以讓線程休眠一段時間后,重新嘗試操作。重試一定次數(shù)依然失敗后,則返回 SQLITE_BUSY 錯誤碼。

下面這段代碼是 SQLite 默認的 Busy Handler

3.1.3 Busy Retry 方案的不足

上面介紹了 SQLite 多線程并發(fā)方案,接下來我們把焦點放在 Busy Retry 這個方案的不足上。

Busy Retry 的方案雖然基本能解決問題,但對性能的壓榨做的不夠極致。在 Retry 過程中,休眠時間的長短和重試次數(shù),是決定性能和操作成功率的關鍵。

然而,它們的最優(yōu)值,因不同操作不同場景而不同。若休眠時間太短或重試次數(shù)太多,會空耗 CPU 的資源;若休眠時間過長,會造成等待的時間太長;若重試次數(shù)太少,則會降低操作的成功率。如下圖


可以看到

  • CPU空轉那段,線程一操作還沒結束,這里空耗了 CPU 的資源
  • 線程閑置那段,線程一已經(jīng)結束,而線程二仍在等待,空耗了時間

3.1.3 開始改造

當 OS 層進行 lock 操作時:

  • 1.通過 pthread_mutex_lock 進行線程鎖,防止其他線程介入。然后比較狀態(tài)量,若當前狀態(tài)不可跳轉,則將當前期望跳轉的狀態(tài),插入到一個 FIFO 的 Queue 尾部。最后,線程通過 pthread_cond_wait 進入 休眠狀態(tài),等待其他線程的喚醒。
  • 2.忽略文件鎖

當 OS 層的 unlock 操作結束后:

取出 Queue 頭部的狀態(tài)量,并比較狀態(tài)是否能夠跳轉。若能夠跳轉,則通過 pthread_cond_signal_thread_np 喚醒對應的線程重試。



新的方案可以在 DB 空閑時的第一時間,通知到其他正在等待的線程,最大程度地降低了空等待的時間,且準確無誤。

此外,由于 Queue 的存在,當主線程被其他線程阻塞時,可以將主線程的操作“插隊”到 Queue 的頭部。當其他線程發(fā)起喚醒通知時,主線程可以有更高的優(yōu)先級,從而降低用戶可感知的卡頓

3.2 I/O 性能優(yōu)化

提到 I/O 效率的提升,最容易想到的就是 mmap了,它可以減少數(shù)據(jù)從 kernel 層到 user 層的數(shù)據(jù)拷貝,從而提高效率。

SQLite 不僅支持 mmap,而且推薦使用,在大多數(shù)平臺是在一定程度上默認打開的。然而早期的 iOS 版本的存在一些 bug,SQLite 在編譯層就關閉了在 iOS 上對 mmap 的支持,并且后知后覺地在16年1月才重新打開。所以如果使用的 SQLite 版本較低,還需注釋掉相關代碼后,重新編譯生成后,才可以享受上 mmap 的性能。

主要修改了

  • 1.數(shù)據(jù)庫關閉并 checkpoint 成功時,不再 truncate 或刪除 WAL 文件,只修改 WAL 的文件頭的 Magic Number。下次數(shù)據(jù)庫打開時, SQLite 會識別到 WAL 文件不可用,重新從頭開始寫入。
  • 2.為 WAL 添加 mmap 的支持

3.3 其他優(yōu)化

禁用文件鎖

如我們在多線程優(yōu)化時所說,對于 iOS app 并沒有多進程的需求。因此我們可以直接注釋掉 os_unix.c 中所有文件鎖相關的操作。也許你會很奇怪,雖然沒有文件鎖的需求,但這個操作耗時也很短,是否有必要特意優(yōu)化呢?其實并不全然。耗時多少是比出來。

SQLite 中有 cache 機制。被加載進內存的 page,使用完畢后不會立刻釋放。而是在一定范圍內通過 LRU 的算法更新 page cache。這就意味著,如果 cache 設置得當,大部分讀操作不會讀取新的 page。然而因為文件鎖的存在,本來只需在內存層面進行的讀操作,不得不進行至少一次 I/O 操作。而我們知道,I/O 操作是遠遠慢于內存操作的。

禁用內存統(tǒng)計鎖

SQLite 會對申請的內存進行統(tǒng)計,而這些統(tǒng)計的數(shù)據(jù)都是放到同一個全局變量里進行計算的。這就意味著統(tǒng)計前后,都是需要加線程鎖,防止出現(xiàn)多線程問題的。

以下 SQLite 內存申請的函數(shù)可以看到,當內存統(tǒng)計打開時,會跑代碼的第二個 if,malloc 的前后被鎖保護了起來。



其實這里內存申請的量不大,并不是非常耗時的操作,但卻很頻繁。多線程并發(fā)時,各線程很容易互相阻塞。因為耗時很短,所以被阻塞的時間也很短暫。似乎不會有太大問題。但頻繁地阻塞卻意味著線程不斷地切換,這是個很影響性能的操作,尤其對于單核設備。

因此,如果不需要內存統(tǒng)計的特性,可以通過 sqlite3_config(SQLITE_CONFIG_MEMSTATUS, 0)進行關閉。這個修改雖然不需要改動源碼,但如果不查看源碼,恐怕是比較難發(fā)現(xiàn)的。

多線程并發(fā)優(yōu)化使得卡頓率從4.08%降至0.19,I/O 優(yōu)化使得讀卡頓從1.50%降至0.20%,寫卡頓從1.18%降至0.21%

4 從FMDB遷移到WCDB好處

4.1 語法上

由于 FMDB 和 WCDB 都基于 SQLite ,因此兩者在數(shù)據(jù)庫的文件格式上一致。用 FMDB 創(chuàng)建、操作的數(shù)據(jù)庫,可以直接通過 WCDB 打開、使用。因此開發(fā)者無需做額外的數(shù)據(jù)遷移

WCDB通過WINQ抽象SQLite語法規(guī)則,使得開發(fā)者可以告別字符串拼接的膠水代碼。通過和接口層的ORM結合,使得即便是很復雜的查詢,也可以通過一行代碼完成,更少的代碼量通常意味著更快的開發(fā)效率和更少的錯誤。并借助IDE的代碼提示和編譯檢查的特性,大大提升了開發(fā)效率。同時還內建了反注入的保護

OC語法式的ORM建模



查詢操作



插入操作

as重定向


鏈式調用


多表查詢


類字段綁定


4.2 方便的數(shù)據(jù)庫升級

WCDB 將數(shù)據(jù)庫升級和 ORM 結合起來,對于需要增刪改的字段,只需直接在 ORM 層面修改,并再次調用 createTableAndIndexesOfName:withClass: 接口即可自動升級

4.3 安全的多線程操作

WCDB 與 FMDB 都支持多線程操作。
在 FMDB 內,當開發(fā)者需要進行多線程操作時,需要使用另外一個類 FMDatabasePool來進行操作。
而 WCDB 基礎的 CRUD 接口都支持多線程,因此開發(fā)者不需要額外關心線程安全的問題。同樣的, WCDB 多線程使用的代碼量也比 FMDB 少得多

4.4 更多

  • WCDB 寫操作優(yōu)于 FMDB 28%、批量寫操作優(yōu)于 FMDB 180%; WCDB 的初始化速度有 107% 的性能優(yōu)勢
  • WCDB內建了對SQL注入的保護
  • WCDB 基于 SQLCipher 提供了加密功能
  • WCDB 內提供統(tǒng)計的接口注冊獲取數(shù)據(jù)庫操作的 SQL 、性能、錯誤等,開發(fā)者可以將這些信息打印到日志或上報到后臺,以調試或統(tǒng)計
  • WCDB 提供了數(shù)據(jù)庫修復工具,以應對數(shù)據(jù)庫損壞無法使用的極端情況。

5.遷移方案

5.1 逐步遷移

使用WCDB新建一個數(shù)據(jù)庫,和老數(shù)據(jù)庫共存,一步一步遷移過來

優(yōu)點: 可以一步一步慢慢遷移過去,一個表一個表遷移;能保存原始數(shù)據(jù)

缺點: 周期長,需要考慮老版本數(shù)據(jù)庫遷移時;-適配問題

5.2 暴力遷移

刪除原應用所有數(shù)據(jù),讓用戶重新登錄,使用WCDB建完所有表和操作

優(yōu)點: 快、準、狠

缺點: 太狠了,對用戶有一定影響,同時一個版本把所有數(shù)據(jù)庫操作全部替換有一定的人力成本

5.3 直接框架遷移(推薦)

直接將以前FMDB的寫法全部轉換為WCDB的寫法,一步到位

優(yōu)點: 能保存原始數(shù)據(jù)

缺點: 跟方法2一樣,有一定的人力成本;需要考慮到老版本數(shù)據(jù)庫適配問題

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,250評論 6 530
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 97,923評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,041評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,475評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,253評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,801評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,882評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,023評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,530評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,494評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,639評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,177評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,890評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,289評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,552評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,242評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,626評論 2 370

推薦閱讀更多精彩內容