一 . Sqlite多線程概述
SQLite 支持三種線程模式:
1. 單線程模式
? ? ? ?這種模式下,沒有進(jìn)行互斥,多線程使用不安全
2. 多線程模式
? ? ? ?這種模式下,在多線程中使用單個數(shù)據(jù)庫連接是不安全的,否則就是安全的。(譯注:即不能在多個線程中共享數(shù)據(jù)庫連接)
3. 串行模式
? ? ? ?這種模式下,sqlite是線程安全的。(譯注:即使在多個線程中不加互斥的使用同一個數(shù)據(jù)庫連接)
? ? ? ?線程模式可以在編譯時(通過源碼編譯sqlite庫時)、啟動時(使用sqlite的應(yīng)用程序初始化時)或者運(yùn)行時(創(chuàng)建數(shù)據(jù)庫連接時)來指定。一般而言,運(yùn)行時指定的模式將覆蓋啟動時的指定模式,啟動時指定的模式將覆蓋編譯時指定的模式。但是,單線程模式一旦被指定,將無法被覆蓋。
? ? ? ?默認(rèn)的線程模式是串行模式。 ?
編譯時選擇線程模式
? ? ? ?可以通過定義SQLITE_THREADSAFE宏來指定線程模式。如果沒有指定,默認(rèn)為串行模式。定義宏SQLITE_THREADSAFE=1指定使用串行模式;=0使用單線程模式;=2使用多線程模式。

? ? ? ?sqlite3_threadsafe()函數(shù)的返回值可以確定編譯時指定的線程模式。如果指定了單線程模式,函數(shù)返回false。如果指定了串行或者多線程模式,函數(shù)返回true。由于sqlite3_threadsafe()函數(shù)要早于多線程模式以及啟動時和運(yùn)行時的模式選擇,所以它既不能區(qū)別多線程模式和串行模式也不能區(qū)別啟動時和運(yùn)行時的模式。
譯注:最后一句可通過sqlite3_threadsafe函數(shù)的實(shí)現(xiàn)來理解
SQLITE_API int sqlite3_threadsafe(void){ return SQLITE_THREADSAFE; }
? ? ? ?如果編譯時指定了單線程模式,那么臨界互斥邏輯在構(gòu)造時就被省略,因此也就無法在啟動時或運(yùn)行時指定串行模式或多線程模式。
啟動時選擇線程模式
? ? ? ?假如在編譯時沒有指定單線程模式,就可以在應(yīng)用程序初始化時使用sqlite3_config()函數(shù)修改線程模式。參數(shù)SQLITE_CONFIG_SINGLETHREAD可指定為單線程模式,SQLITE_CONFIG_MULTITHREAD指定為多線程模式,SQLITE_CONFIG_SERIALIZED指定為串行模式。
運(yùn)行時選擇線程模式
? ? ? ?如果沒有在編譯時和啟動時指定為單線程模式,那么每個數(shù)據(jù)庫連接在創(chuàng)建時可單獨(dú)的被指定為多線程模式或者串行模式,但是不能指定為單線程模式。如果在編譯時或啟動時指定為單線程模式,就無法在創(chuàng)建連接時指定多線程或者串行模式。
? ? ? ?創(chuàng)建連接時用sqlite3_open_v2()函數(shù)的第三個參數(shù)來指定線程模式。SQLITE_OPEN_NOMUTEX標(biāo)識創(chuàng)建多線程模式的連接;SQLITE_OPEN_FULLMUTEX標(biāo)識創(chuàng)建串行模式的連接。如果沒有指定標(biāo)識,或者使用sqlite3_open()或sqlite3_open16()函數(shù)來創(chuàng)建數(shù)據(jù)庫連接,那么在編譯時或啟動時指定的線程模式將作為默認(rèn)的線程模式使用。
二. WAL并發(fā)讀寫
在3.7.0以后,WAL(Write-Ahead Log)模式可以使用,是另一種實(shí)現(xiàn)事務(wù)原子性的方法。
1. WAL的優(yōu)點(diǎn)
? ? ? ?在大多數(shù)情況下更快
? ? ? ?并行性更高。因?yàn)樽x操作和寫操作可以并行。
? ? ? 文件IO更加有序化,串行化(more sequential)
? ? ? ?使用fsync()的次數(shù)更少,在fsync()調(diào)用時好時壞的機(jī)器上較為未定。
缺點(diǎn)
? ? ? ?一般情況下需要VFS支持共享內(nèi)存模式。(shared-memory primitives)
? ? ? ?操作數(shù)據(jù)庫文件的進(jìn)程必須在同一臺主機(jī)上,不能用在網(wǎng)絡(luò)操作系統(tǒng)。
? ? ? ?持有多個數(shù)據(jù)庫文件的數(shù)據(jù)庫連接對于單個數(shù)據(jù)庫時原子的,對于全部數(shù)據(jù)庫是不原子的。
? ? ? ?進(jìn)入WAL模式以后不能修改page的size。
? ? ? ?不能打開只讀的WAL數(shù)據(jù)庫(Read-Only Databases),這進(jìn)程必須有"-shm"文件的寫權(quán)限。
? ? ? ?對于只進(jìn)行讀操作,很少進(jìn)行寫操作的數(shù)據(jù)庫,要慢那么1到2個百分點(diǎn)。
? ? ? ?會有多余的"-wal"和"-shm"文件
? ? ? ?需要開發(fā)者注意checkpointing
2. 原理
? ? ? ?回滾日志的方法是把為改變的數(shù)據(jù)庫文件內(nèi)容寫入日志里,然后把改變后的內(nèi)容直接寫到數(shù)據(jù)庫文件中去。在系統(tǒng)crash或掉電的情況下,日志里的內(nèi)容被重新寫入數(shù)據(jù)庫文件中。日志文件被刪除,標(biāo)志commit著一次commit的結(jié)束。
? ? ? ?WAL模式于此此相反。原始為改變的數(shù)據(jù)庫內(nèi)容在數(shù)據(jù)庫文件中,對數(shù)據(jù)庫文件的修改被追加到單獨(dú)的WAL文件中。當(dāng)一條記錄被追加到WAL文件后,標(biāo)志著一次commit的結(jié)束。因此一次commit不必對數(shù)據(jù)庫文件進(jìn)行操作,當(dāng)正在進(jìn)行寫操作時,可以同時進(jìn)行讀操作。多個事務(wù)的內(nèi)容可以追加到一個WAL文件的末尾。
checkpoint
? ? ? ?最后WAL文件的內(nèi)容必須更新到數(shù)據(jù)庫文件中。把WAL文件的內(nèi)容更新到數(shù)據(jù)庫文件的過程叫做一次checkpoint。
? ? ? ?回滾日志的方法有兩種操作:讀和寫。WAL有三種操作,讀、寫和checkpoint。
? ? ? ?默認(rèn)的,SQL會在WAL文件達(dá)到1000page時進(jìn)行一次checkpoint。進(jìn)行WAL的時機(jī)也可以由應(yīng)用程序自己決定。
并發(fā)性
? ? ? ?當(dāng)一個讀操作發(fā)生在WAL模式的數(shù)據(jù)庫上時,會首先找到WAL文件中最后一次提交,叫做"end mark"。每一個事務(wù)可以有自己的"end point",但對于一個給定額事務(wù)來說,end mark是固定的。
? ? ? ?當(dāng)讀取數(shù)據(jù)庫中的page時,SQLite會先從WAL文件中尋找有沒有對應(yīng)的page,從找出離end mark最近的那一條記錄;如果找不到,那么就從數(shù)據(jù)庫文件中尋找對一個的page。為了避免每次事務(wù)都要掃描一遍WAL文件,SQLite在共享內(nèi)存中維護(hù)了一個"wal-index"的數(shù)據(jù)結(jié)構(gòu),幫助快速定位page。
? ? ? ?寫數(shù)據(jù)庫只是把新內(nèi)容加到WAL文件的末尾,和讀操作沒有關(guān)系。由于只有一個WAL文件,因此同時只能有一個寫操作。
? ? ? ?checkpoint操作可以和讀操作并行。但是如果checkpoint把一個page寫入數(shù)據(jù)庫文件,而且這個page超過了當(dāng)前讀操作的end mark時,checkpoint必須停止。否則會把當(dāng)前正在讀的部分覆蓋掉。下次checkpoint時,會從這個page開始往數(shù)據(jù)庫中拷貝數(shù)據(jù)。
? ? ? ?當(dāng)寫操作時,會檢查WAL文件被拷貝到數(shù)據(jù)庫的進(jìn)度。如果已經(jīng)完全被拷貝到數(shù)據(jù)庫文件中,已經(jīng)同步,并且沒有讀操作在使用WAL文件,那么會把WAL文件清空,從其實(shí)開始追加數(shù)據(jù)。保證WAL文件不會無限制增長。
性能
? ? ? ?寫操作是很快的,因?yàn)橹恍枰M(jìn)行一次寫操作,并且是順序的(不是隨機(jī)的,每次都寫到末尾)。而且,把數(shù)據(jù)刷到磁盤上是不必須的。(如果PRAGMA synchronous是FULL,每次commit要刷一次,否則不刷。)
? ? ? ?讀操作的性能有所下降,因?yàn)樾枰獜腤AL文件中查找內(nèi)容,花費(fèi)的時間和WAL文件的大小有 關(guān)。wal-index可以縮短這個時間,但是也不能完全避免。因此需要保證WAL文件的不會太大。
? ? ? ?為了保護(hù)數(shù)據(jù)庫不被損壞,需要在把WAL文件寫入數(shù)據(jù)庫之前把WAL文件刷入磁盤;在重置WAL文件之前要把數(shù)據(jù)庫內(nèi)容刷入數(shù)據(jù)庫文件。此外checkpoint需要查找操作。這些因素使得checkpoint比寫操作慢一些。
? ? ? ?默認(rèn)策略是很多線程可以增長WAL文件。把WAL文件大小變得比1000page大的那個線程要負(fù)責(zé)進(jìn)行checkpoint。會導(dǎo)致絕大部分讀寫操作都是很快的,隨機(jī)有一個寫操作非常慢。也可以禁用自動checkpoint的策略,定期在一個線程或進(jìn)程中進(jìn)行checkpoint操作。
? ? ? ?高效的寫操作希望WAL文件越大越好;高效的讀操作希望WAL文件越小越好。兩者存在一個tradeoff。
3. 激活和配置WAL模式
? ? ? ?執(zhí)行命令:PRAGMA journal_mode=WAL;,如果成功,會返回"wal"。

? ? ? ?自動checkpoint
? ? ? ?可以手動checkpoint同步wal文件數(shù)據(jù)到數(shù)據(jù)庫中

? ? ? ?sqlite3_wal_checkpoint(sqlite3*db, const char *zDb)
配置checkpoint
? ? ? ?sqlite3_wal_autocheckpoint(sqlite3 *db,intN);
? ? ? ?Application-Initiated Checkpoints
? ? ? ?可以在任意一個可以進(jìn)行寫操作的數(shù)據(jù)庫連接中調(diào)用sqlite3_wal_checkpoint_v2()或sqlite3_wal_checkpoint()。
WAL模式的持久性
? ? ? ?當(dāng)一個進(jìn)程設(shè)置了WAL模式,關(guān)閉這個進(jìn)程,重新打開這個數(shù)據(jù)庫,仍然是WAL模式。
如果在一個數(shù)據(jù)庫連接中設(shè)置了WAL模式,那么這個數(shù)據(jù)庫的所有連接都將被設(shè)為WAL模式。
4. 只讀數(shù)據(jù)庫
? ? ? ?如果數(shù)據(jù)庫需要恢復(fù),而你只有讀權(quán)限,沒有寫權(quán)限,那么你不能讀取這個數(shù)據(jù)庫,因?yàn)檫M(jìn)行讀操作的第一步就是恢復(fù)數(shù)據(jù)庫。
? ? ? ?類似的,因?yàn)閃AL模式下的數(shù)據(jù)庫進(jìn)行讀操作時,需要類似數(shù)據(jù)庫恢復(fù)的操作,因此如果只有讀權(quán)限,也不能對打開數(shù)據(jù)庫。
? ? ? ?WAL的實(shí)現(xiàn)需要有一個基于WAL文件的哈希表在共享內(nèi)存中。在Unix和Windows的VFS實(shí)現(xiàn)中,是基于MMap的。將共享內(nèi)存映射到同目錄下的"-shm"文件中。因此即使是對WAL模式下的數(shù)據(jù)庫文件進(jìn)行讀操作,也需要寫權(quán)限。
? ? ? ?為了把數(shù)據(jù)庫文件轉(zhuǎn)化為只讀的文件,需要先把這個數(shù)據(jù)庫的日志模式改為"delete".
5. 避免過大的WAL文件
6. WAL-index的共享內(nèi)存實(shí)現(xiàn)
? ? ? ?在WAL發(fā)布之前,曾經(jīng)嘗試過將wal-index映射到臨時目錄,如/dev/shm或/tmp。但是不同的用戶看到的目錄是不同的,所以此路不通。
? ? ? ?后來嘗試將wal-index映射到匿名的虛擬內(nèi)存塊中,但是無法在不用的Unix版本中保持一致。
? ? ? ?最終決定采用將wal-index映射到同目錄下。這樣子會導(dǎo)致不必要的磁盤IO。但是問題不大,是因?yàn)閣al-index很少超過32k,而且從不會調(diào)用sync操作。此外,最后一個數(shù)據(jù)庫連接關(guān)閉以后,這個文件會被刪除。
? ? ? ?如果這個數(shù)據(jù)庫只會被一個進(jìn)程使用,那么可以使用heap memory而不是共享內(nèi)存。
7. 不用共享內(nèi)存實(shí)現(xiàn)WAL
? ? ? ?在3.7.4版本以后,只要SQLite的lock mode被設(shè)為EXCLUSIVE,那么即使共享內(nèi)存不支持,也可以使用WAL模式。
? ? ? ?換句話說,如果只有一個進(jìn)程使用SQLite,那么不用共享內(nèi)存也可以使用WAL。
? ? ? ?此時,將lock mode改為normal是無效的,需要實(shí)現(xiàn)取消WAL模式。
總結(jié):總過最近對sqlite3的學(xué)習(xí),移動端的數(shù)據(jù)庫最然不如后臺數(shù)據(jù)庫那么復(fù)雜,但也存在著很多可以發(fā)掘和優(yōu)化的技術(shù)點(diǎn)。這次嘗試了對sqlite3多線程操作和并發(fā)讀寫的優(yōu)化,希望后續(xù)還可以學(xué)習(xí)到更好的優(yōu)化方案。
作者:Olivia_Zqy