SQLite在多線程環境下的應用


文一

SQLite的FAQ里面已經專門說明,先貼出來。供以后像我目前的入門者學習。



(7) 多個應用程序或者同一個應用程序的多個例程能同時存取同一個數據庫文件嗎?

多進程可以同時打開同一個數據庫,也可以同時 SELECT 。但只有一個進程可以立即改數據庫。

SQLite使用讀/寫鎖定來控制數據庫訪問。(Win95/98/ME 操作系統缺乏讀/寫鎖定支持,在低於 2.7.0 的版本中,這意味着在 windows 下在同一時間內只能有一個進程讀數據庫。在版本 2.7.0 中 這個問題通過在 windows 接口代碼中執行一個用戶間隔幾率讀寫鎖定策略解決了。) 但如果數據庫文件在一個 NFS 文件系統中,控制並發讀書的鎖定機制可以會出錯。因為 NFS 的fcntl() 文件鎖定有時會出問題。如果有多進程可能並發讀數據庫則因當避免把數據庫文件放在 NFS 文件系統中。 根據微軟的文檔,如果不運行 Share.exe 后台程序則 FAT 文件系統中的鎖定可能不工作。對 Windows 非常有經驗的人告訴我網絡文件的鎖定有許多問題並且不可靠。如果是這樣,在2個或以上 Windows 系統中共享一個 SQLite 數據庫文件會導致不可預知的問題。

我們知道沒有其他的嵌入式 SQL數據庫引擎比SQLite支持更多的並發性。 SQLite允許多進程 同時打開和讀取數據庫。任何一個進程需要寫入時,整個數據庫將在這一過程中被鎖定。但這一般僅耗時 幾毫秒。其他進程只需等待然后繼續其他事務。其他嵌入式SQL數據庫引擎往往只允許單進程訪問數據庫。

但是,client/server型的數據庫引擎 (如 PostgreSQL, MySQL, 以及 Oracle) 通常支持更高的並發度, 並支持多進程同時寫入同一個數據庫。由於總有一個控制良好的服務器協調數據庫的訪問,這才保證了以上 特性的實現。如果你的應用需要很高的並發度,你應該考慮使用client/server數據庫。事實上,經驗告訴 我們大多數應用所需要的並發度比他們的設計者們想象的要少得多。

當 SQLite 嘗試操作一個被另一個進程鎖定的文件時,缺省的行為是返回 SQLITE_BUSY。你可以用 C代碼更改這一行為。 使用 sqlite3_busy_handler() 或sqlite3_busy_timeout() API函數。

如果兩個或更多進程同時打開同一個數據庫,其中一個進程創建了新的表或索引,則其它進程可能不能立即看見新的表。其它進程可能需要關閉並重新連結數據庫。


--------------------------------------------------------------------------------

(8) SQLite是線程安全的嗎?

有時候是的。為了線程安全,SQLite 必須在編譯時把 THREADSAFE 預處理宏設為1。在缺省的發行的已編譯版本中 Windows 版的是線程安全的,而 Linux 版的不是。如果要求線程安全,Linux 版的要重新編譯。

“線程安全”是指二個或三個線程可以同時調用獨立的不同的sqlite3_open() 返回的"sqlite3"結構。而不是在多線程中同時使用同一個 sqlite3 結構指針。

一個sqlite3結構只能在調用 sqlite3_open創建它的那個進程中使用。你不能在一個線程中打開一個數據庫然后把指針傳遞給另一個線程使用。這是因為大多數多線程系統的限制(或 Bugs?)例如RedHat9上。在這些有問題的系統上,一個 線程創建的fcntl()鎖不能由另一個線程刪除或修改。由於SQLite依賴fcntl()鎖來進行並發控制,當在線程間傳遞數據庫連接時會出現嚴重的問題。

也許在Linux下有辦法解決fcntl()鎖的問題,但那十分復雜並且對於正確性的測試將是極度困難的。因此,SQLite目前不允許在線程間共享句柄。

在UNIX下,你不能通過一個 fork() 系統調用把一個打開的 SQLite 數據庫放入子過程中,否則會出錯。

 

文二

這幾天研究了一下SQLite這個嵌入式數據庫在多線程環境下的應用,感覺里面的學問還挺多,於是就在此分享一下。


先說下初衷吧,實際上我經常看到有人抱怨SQLite不支持多線程。而在iOS開發時,為了不阻塞主線程,數據庫訪問必須移到子線程中。為了解決這個矛盾,很有必要對此一探究竟。

關於這個問題,最權威的解答當然是SQLite官網上的“Is SQLite threadsafe?”這個問答。
簡單來說,從3.3.1版本開始,它就是線程安全的了。而iOS的SQLite版本沒有低於這個版本的:

3.4.0 - iPhone OS 2.2.1
3.6.12 - iPhone OS 3.0 / 3.1
3.6.22 - iPhone OS 4.0
3.6.23.2 - iPhone OS 4.1 / 4.2
3.7.2 - iPhone OS 4.3
3.7.7 - iPhone OS 5.0

當然,你也可以自己編譯最新版本。只是我發現自己編譯出來的3.7.8居然比iOS 4.3.3內置的3.7.2慢了一半,不知道蘋果做了什么優化。發現是我編譯成了debug版本,改成release后性能比內置版本高5%左右,不過構建出來的app會大420k左右。

不過這個線程安全仍然是有限制的,在這篇《Is SQLite thread-safe?》里有詳細的解釋。
另一篇重要的文檔就是《SQLite And Multiple Threads》。它指出SQLite支持3種線程模式:

  1. 單線程:禁用所有的mutex鎖,並發使用時會出錯。當SQLite編譯時加了SQLITE_THREADSAFE=0參數,或者在初始化SQLite前調用sqlite3_config(SQLITE_CONFIG_SINGLETHREAD)時啟用。
  2. 多線程:只要一個數據庫連接不被多個線程同時使用就是安全的。源碼中是啟用bCoreMutex,禁用bFullMutex。實際上就是禁用數據庫連接和prepared statement(准備好的語句)上的鎖,因此不能在多個線程中並發使用同一個數據庫連接或prepared statement。當SQLite編譯時加了SQLITE_THREADSAFE=2參數時默認啟用。若SQLITE_THREADSAFE不為0,可以在初始化SQLite前,調用sqlite3_config(SQLITE_CONFIG_MULTITHREAD)啟用;或者在創建數據庫連接時,設置SQLITE_OPEN_NOMUTEX flag。
  3. 串行:啟用所有的鎖,包括bCoreMutex和bFullMutex。因為數據庫連接和prepared statement都已加鎖,所以多線程使用這些對象時沒法並發,也就變成串行了。當SQLite編譯時加了SQLITE_THREADSAFE=1參數時默認啟用。若SQLITE_THREADSAFE不為0,可以在初始化SQLite前,調用sqlite3_config(SQLITE_CONFIG_SERIALIZED)啟用;或者在創建數據庫連接時,設置SQLITE_OPEN_FULLMUTEX flag。

而這里所說的初始化是指調用sqlite3_initialize()函數,這個函數在調用sqlite3_open()時會自動調用,且只有第一次調用是有效的。
另一個要說明的是prepared statement,它是由數據庫連接(的pager)來管理的,使用它也可看成使用這個數據庫連接。因此在多線程模式下,並發對同一個數據庫連接調用sqlite3_prepare_v2()來創建prepared statement,或者對同一個數據庫連接的任何prepared statement並發調用sqlite3_bind_*()和sqlite3_step()等函數都會出錯(在iOS上,該線程會出現EXC_BAD_ACCESS而中止)。這種錯誤無關讀寫,就是只讀也會出錯。文檔中給出的安全使用規則是:沒有事務正在等待執行,所有prepared statement都被finalized
順帶一提,調用sqlite3_threadsafe()可以獲得編譯期的SQLITE_THREADSAFE參數。標准發行版是1,也就是串行模式;而iOS上是2,也就是多線程模式;Python的sqlite3模塊也默認使用串行模式,可以用sqlite3.threadsafety來配置。但是默認情況下,一個線程只能使用當前線程打開的數據庫連接,除非在連接時設置了check_same_thread=False參數。

現在3種模式都有所了解了,清楚SQLite並不是對多線程無能為力后,接下來就了解下事務吧。
數據庫只有在事務中才能被更改。所有更改數據庫的命令(除SELECT以外的所有SQL命令)都會自動開啟一個新事務,並且當最后一個查詢完成時自動提交。
而BEGIN命令可以手動開始事務,並關閉自動提交。當下一條COMMIT命令執行時,自動提交再次打開,事務中所做的更改也被寫入數據庫。當COMMIT失敗時,自動提交仍然關閉,以便讓用戶嘗試再次提交。若執行的是ROLLBACK命令,則也打開自動提交,但不保存事務中的更改。關閉數據庫或遇到錯誤時,也會自動回滾事務。
經常有人抱怨SQLite的插入太慢,實際上它可以做到每秒插入幾萬次,但是每秒只能提交幾十次事務。因此在插入大批數據時,可以通過禁用自動提交來提速。

事務在改寫數據庫文件時,會先生成一個rollback journal(回滾日志),記錄初始狀態(其實就是備份),所有改動都是在數據庫文件上進行的。當事務需要回滾時,可以將備份文件的內容還原到數據庫文件;提交成功時,默認的delete模式下會直接刪除這個日志。這個日志也可以幫助解決事務執行過程中斷電,導致數據庫文件損壞的問題。但如果操作系統或文件系統有bug,或是磁盤損壞,則仍有可能無法恢復。
而從3.7.0版本(對應iOS 4.3)開始,SQLite還提供了Write-Ahead Logging模式。與delete模式相比,WAL模式在大部分情況下更快,並發性更好,讀和寫之間互不阻塞;而其缺點對於iPhone這種嵌入式設備來說可以忽略,只需注意不要以只讀方式打開WAL模式的數據庫即可。
使用WAL模式時,改寫操是附加(append)到WAL文件,而不改動數據庫文件,因此數據庫文件可以被同時讀取。當執行checkpoint操作時,WAL文件的內容會被寫回數據庫文件。當WAL文件達到SQLITE_DEFAULT_WAL_AUTOCHECKPOINT(默認值是1000)頁(默認大小是1KB)時,會自動使用當前COMMIT的線程來執行checkpoint操作。也可以關閉自動checkpoint,改為手動定期checkpoint。
為了避免讀取的數據不一致,查詢時也需要讀取WAL文件,並記錄一個結尾標記(end mark)。這樣的代價就是讀取會變得稍慢,但是寫入會變快很多。要提高查詢性能的話,可以減小WAL文件的大小,但寫入性能也會降低。
需要注意的是,低版本的SQLite不能讀取高版本的SQLite生成的WAL文件,但是數據庫文件是通用的。這種情況在用戶進行iOS降級時可能會出現,可以把模式改成delete,再改回WAL來修復。
要對一個數據庫連接啟用WAL模式,需要執行“PRAGMA journal_mode=WAL;”這條命令,它的默認值是“journal_mode=DELETE”。執行后會返回新的journal_mode字符串值,即成功時為"wal",失敗時為之前的模式(例如"delete")。一旦啟用WAL模式后,數據庫會保持這個模式,這樣下次打開數據庫時仍然是WAL模式。
要停止自動checkpoint,可以使用wal_autocheckpoint指令或sqlite3_wal_checkpoint()函數。手動執行checkpoint可以使用wal_checkpoint指令或sqlite3_wal_checkpoint()函數。

還有一個很重要的知識點需要強調:事務是和數據庫連接相關的,每個數據庫連接(使用pager來)維護自己的事務,且同時只能有一個事務(但是可以用SAVEPOINT來實現內嵌事務)。
也就是說,事務與線程無關,一個線程里可以同時用多個數據庫連接來完成多個事務,而多個線程也可以同時(非並發)使用一個數據庫連接來共同完成一個事務。
下面用Python來演示一下:

# -*- coding: utf-8 -*- import sqlite3 import threading def f(): con.rollback() con = sqlite3.connect('test.db', check_same_thread=False) # 允許在其他線程中使用這個連接 cu = con.cursor() cu.execute('CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY)') print cu.execute('SELECT count(*) FROM test').fetchone()[0] # 0 cu.execute('INSERT INTO test VALUES (NULL)') print cu.execute('SELECT count(*) FROM test').fetchone()[0] # 1 thread = threading.Thread(target=f) thread.start() thread.join() print cu.execute('SELECT count(*) FROM test').fetchone()[0] # 0 cu.close() con.close()

在這個例子中,雖然是在子線程中執行rollback,但由於和主線程用的是同一個數據庫連接,所以主線程所做的更改也被回滾了。
而如果是用不同的數據庫連接,每個連接都不能讀取其他連接中未提交的數據,除非使用read-uncommitted模式。

而要實現事務,就不得不用到
一個SQLite數據庫文件有5種鎖的狀態:

  • UNLOCKED:表示數據庫此時並未被讀寫。
  • SHARED:表示數據庫可以被讀取。SHARED鎖可以同時被多個線程擁有。一旦某個線程持有SHARED鎖,就沒有任何線程可以進行寫操作。
  • RESERVED:表示准備寫入數據庫。RESERVED鎖最多只能被一個線程擁有,此后它可以進入PENDING狀態。
  • PENDING:表示即將寫入數據庫,正在等待其他讀線程釋放SHARED鎖。一旦某個線程持有PENDING鎖,其他線程就不能獲取SHARED鎖。這樣一來,只要等所有讀線程完成,釋放SHARED鎖后,它就可以進入EXCLUSIVE狀態了。
  • EXCLUSIVE:表示它可以寫入數據庫了。進入這個狀態后,其他任何線程都不能訪問數據庫文件。因此為了並發性,它的持有時間越短越好。

一個線程只有在擁有低級別的鎖的時候,才能獲取更高一級的鎖。SQLite就是靠這5種類型的鎖,巧妙地實現了讀寫線程的互斥。同時也可看出,寫操作必須進入EXCLUSIVE狀態,此時並發數被降到1,這也是SQLite被認為並發插入性能不好的原因。
另外,read-uncommitted和WAL模式會影響這個鎖的機制。在這2種模式下,讀線程不會被寫線程阻塞,即使寫線程持有PENDING或EXCLUSIVE鎖。

提到鎖就不得不說到死鎖的問題,而SQLite也可能出現死鎖。
下面舉個例子:

連接1:BEGIN (UNLOCKED)
連接1:SELECT ... (SHARED)
連接1:INSERT ... (RESERVED)
連接2:BEGIN (UNLOCKED)
連接2:SELECT ... (SHARED)
連接1:COMMIT (PENDING,嘗試獲取EXCLUSIVE鎖,但還有SHARED鎖未釋放,返回SQLITE_BUSY)
連接2:INSERT ... (嘗試獲取RESERVED鎖,但已有PENDING鎖未釋放,返回SQLITE_BUSY)

現在2個連接都在等待對方釋放鎖,於是就死鎖了。當然,實際情況並沒那么糟糕,任何一方選擇不繼續等待,回滾事務就行了。

不過要更好地解決這個問題,就必須更深入地了解事務了。
實際上BEGIN語句可以有3種起始狀態:

  • DEFERRED:默認值,開始事務時不獲取任何鎖。進行第一次讀操作時獲取SHARED鎖,進行第一次寫操作時獲取RESERVED鎖。
  • IMMEDIATE:開始事務時獲取RESERVED鎖。
  • EXCLUSIVE:開始事務時獲取EXCLUSIVE鎖。


現在考慮2個事務在開始時都使用IMMEDIATE方式:

連接1:BEGIN IMMEDIATE (RESERVED)
連接1:SELECT ... (RESERVED)
連接1:INSERT ... (RESERVED)
連接2:BEGIN IMMEDIATE (嘗試獲取RESERVED鎖,但已有RESERVED鎖未釋放,因此事務開始失敗,返回SQLITE_BUSY,等待用戶重試)
連接1:COMMIT (EXCLUSIVE,寫入完成后釋放)
連接2:BEGIN IMMEDIATE (RESERVED)
連接2:SELECT ... (RESERVED)
連接2:INSERT ... (RESERVED)
連接2:COMMIT (EXCLUSIVE,寫入完成后釋放)

這樣死鎖就被避免了。

而EXCLUSIVE方式則更為嚴苛,即使其他連接以DEFERRED方式開啟事務也不會死鎖:

連接1:BEGIN EXCLUSIVE (EXCLUSIVE)
連接1:SELECT ... (EXCLUSIVE)
連接1:INSERT ... (EXCLUSIVE)
連接2:BEGIN (UNLOCKED)
連接2:SELECT ... (嘗試獲取SHARED鎖,但已有EXCLUSIVE鎖未釋放,返回SQLITE_BUSY,等待用戶重試)
連接1:COMMIT (EXCLUSIVE,寫入完成后釋放)
連接2:SELECT ... (SHARED)
連接2:INSERT ... (RESERVED)
連接2:COMMIT (EXCLUSIVE,寫入完成后釋放)

不過在並非很高的情況下,直接獲取EXCLUSIVE鎖的難度比較大;而且為了避免EXCLUSIVE狀態長期阻塞其他請求,最好的方式還是讓所有寫事務都以IMMEDIATE方式開始。
順帶一提,要實現重試的話,可以使用sqlite3_busy_timeout()或sqlite3_busy_handler()函數。

由此可見,要想保證線程安全的話,可以有這4種方式:

  1. SQLite使用單線程模式,用一個專門的線程訪問數據庫。
  2. SQLite使用單線程模式,用一個線程隊列來訪問數據庫,隊列一次只允許一個線程執行,隊列里的線程共用一個數據庫連接。
  3. SQLite使用多線程模式,每個線程創建自己的數據庫連接。
  4. SQLite使用串行模式,所有線程共用全局的數據庫連接。


接下來就一一測試這幾種方式在iPhone 4(iOS 4.3.3,SQLite 3.7.2)上的性能表現。

第一種方式太過麻煩,需要線程間通信,這里我就忽略了。

第二種方式可以用dispatch_queue_create()來創建一個serial queue,或者用一個maxConcurrentOperationCount為1的NSOperationQueue來實現。
這種方式的缺點就是事務必須在一個block或operation里完成,否則會亂序;而耗時較長的事務會阻塞隊列。另外,沒法利用多核CPU的優勢。

先初始化數據庫:

#import <sqlite3.h> static char dbPath[200]; static sqlite3 *database; static sqlite3 *openDb() { if (sqlite3_open(dbPath, &database) != SQLITE_OK) { sqlite3_close(database); NSLog(@"Failed to open database: %s", sqlite3_errmsg(database)); } return database; } - (void)viewDidLoad { [super viewDidLoad]; sqlite3_config(SQLITE_CONFIG_SINGLETHREAD); NSLog(@"%d", sqlite3_threadsafe()); NSLog(@"%s", sqlite3_libversion()); NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsDirectory = [paths objectAtIndex:0]; strcpy(dbPath, [[documentsDirectory stringByAppendingPathComponent:@"data.sqlite3"] UTF8String]); database = openDb(); char *errorMsg; if (sqlite3_exec(database, "CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY AUTOINCREMENT, value INTEGER);", NULL, NULL, &errorMsg) != SQLITE_OK) { NSLog(@"Failed to create table: %s", errorMsg); } }


再插入1000條測試數據:

static void insertData() { char *errorMsg; if (sqlite3_exec(database, "BEGIN TRANSACTION", NULL, NULL, &errorMsg) != SQLITE_OK) { NSLog(@"Failed to begin transaction: %s", errorMsg); } static const char *insert = "INSERT INTO test VALUES (NULL, ?);"; sqlite3_stmt *stmt; if (sqlite3_prepare_v2(database, insert, -1, &stmt, NULL) == SQLITE_OK) { for (int i = 0; i < 1000; ++i) { sqlite3_bind_int(stmt, 1, arc4random()); if (sqlite3_step(stmt) != SQLITE_DONE) { --i; NSLog(@"Error inserting table: %s", sqlite3_errmsg(database)); } sqlite3_reset(stmt); } sqlite3_finalize(stmt); } if (sqlite3_exec(database, "COMMIT TRANSACTION", NULL, NULL, &errorMsg) != SQLITE_OK) { NSLog(@"Failed to commit transaction: %s", errorMsg); } static const char *query = "SELECT count(*) FROM test;"; if (sqlite3_prepare_v2(database, query, -1, &stmt, NULL) == SQLITE_OK) { if (sqlite3_step(stmt) == SQLITE_ROW) { NSLog(@"Table size: %d", sqlite3_column_int(stmt, 0)); } else { NSLog(@"Failed to read table: %s", sqlite3_errmsg(database)); } sqlite3_finalize(stmt); } }


然后創建一個串行隊列:

static dispatch_queue_t queue; - (void)viewDidLoad { // ... queue = dispatch_queue_create("net.keakon.db", NULL); }


再設置一個計數器,每秒執行一次:

static int lastReadCount = 0; static int readCount = 0; static int lastWriteCount = 0; static int writeCount = 0; - (void)count { int lastRead = lastReadCount; int lastWrite = lastWriteCount; lastReadCount = readCount; lastWriteCount = writeCount; NSLog(@"%d, %d", lastReadCount - lastRead, lastWriteCount - lastWrite); } - (void)viewDidLoad { // ... [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(count) userInfo:nil repeats:YES]; }


這樣就可以開始測試select和update了:

static void readData() { static const char *query = "SELECT value FROM test WHERE value < ? ORDER BY value DESC LIMIT 1;"; void (^ __block readBlock)() = Block_copy(^{ sqlite3_stmt *stmt; if (sqlite3_prepare_v2(database, query, -1, &stmt, NULL) == SQLITE_OK) { sqlite3_bind_int(stmt, 1, arc4random()); int returnCode = sqlite3_step(stmt); if (returnCode == SQLITE_ROW || returnCode == SQLITE_DONE) { ++readCount; } sqlite3_finalize(stmt); } else { NSLog(@"Failed to prepare statement: %s", sqlite3_errmsg(database)); } dispatch_async(queue, readBlock); }); dispatch_async(queue, readBlock); } static void writeData() { static const char *update = "UPDATE test SET value = ? WHERE id = ?;"; void (^ __block writeBlock)() = Block_copy(^{ sqlite3_stmt *stmt; if (sqlite3_prepare_v2(database, update, -1, &stmt, NULL) == SQLITE_OK) { sqlite3_bind_int(stmt, 1, arc4random()); sqlite3_bind_int(stmt, 2, arc4random() % 1000 + 1); if (sqlite3_step(stmt) == SQLITE_DONE) { ++writeCount; } sqlite3_finalize(stmt); } else { NSLog(@"Failed to prepare statement: %s", sqlite3_errmsg(database)); } dispatch_async(queue, writeBlock); }); dispatch_async(queue, writeBlock); }

這里是用dispatch_async()來異步地遞歸調用block。
因為block是在棧里生成的,異步執行時已經被銷毀,所以需要copy到堆。因為需要一直執行,所以我就沒release了。
此外,光copy的話還是無法正常執行,但是把block本身的存儲類型設為__block后就正常了,原因我也不清楚。

測試結果為只讀時平均每秒165次,只寫時每秒68次,同時讀寫時每秒各47次。換成多線程或串行模式時,效率也差不多。

接着試試WAL模式:

if (sqlite3_exec(database, "PRAGMA journal_mode=WAL;", NULL, NULL, &errorMsg) != SQLITE_OK) {
    NSLog(@"Failed to set WAL mode: %s", errorMsg);
}

sqlite3_wal_checkpoint(database, NULL); // 每次測試前先checkpoint,避免WAL文件過大而影響性能

測試結果為只讀時平均每秒166次,只寫時每秒244次,同時讀寫時每秒各97次。並發性增加了1倍有木有!更誇張的是寫入比讀取還快了。

在自編譯的3.7.8版中,同時讀寫為每秒各102次,加上SQLITE_THREADSAFE=0參數后為每秒各104次,性能稍有提升。

第三種方式需要打開和關閉數據庫連接,所以會額外消耗一些時間。此外還要維持各個連接間的互斥,事務也比較容易沖突,但能確保事務正確執行。

首先需要移除全局的database變量,並修改openDb()函數:

static sqlite3 *openDb() { sqlite3 *database = NULL; if (sqlite3_open(dbPath, &database) != SQLITE_OK) { sqlite3_close(database); NSLog(@"Failed to open database: %s", sqlite3_errmsg(database)); } return database; }


再配置成多線程模式:

sqlite3_config(SQLITE_CONFIG_MULTITHREAD);


隊列改成可以亂序執行的:

queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);


然后是訪問數據庫:

static void readData() { static const char *query = "SELECT value FROM test WHERE value < ? ORDER BY value DESC LIMIT 1;"; dispatch_async(queue, ^{ sqlite3 *database = openDb(); sqlite3_stmt *stmt; if (sqlite3_prepare_v2(database, query, -1, &stmt, NULL) == SQLITE_OK) { while (YES) { sqlite3_bind_int(stmt, 1, arc4random()); int returnCode = sqlite3_step(stmt); if (returnCode == SQLITE_ROW || returnCode == SQLITE_DONE) { ++readCount; } sqlite3_reset(stmt); } sqlite3_finalize(stmt); } else { NSLog(@"Failed to prepare statement: %s", sqlite3_errmsg(database)); } sqlite3_close(database); }); } static void writeData() { static const char *update = "UPDATE test SET value = ? WHERE id = ?;"; dispatch_async(queue, ^{ sqlite3 *database = openDb(); sqlite3_stmt *stmt; if (sqlite3_prepare_v2(database, update, -1, &stmt, nil) == SQLITE_OK) { while (YES) { sqlite3_bind_int(stmt, 1, arc4random()); sqlite3_bind_int(stmt, 2, arc4random() % 1000 + 1); if (sqlite3_step(stmt) == SQLITE_DONE) { ++writeCount; } sqlite3_reset(stmt); } sqlite3_finalize(stmt); } else { NSLog(@"Failed to prepare statement: %s", sqlite3_errmsg(database)); } sqlite3_close(database); }); }

這里就無需遞歸調用了,直接在子線程中循環即可。

測試結果為只讀時平均每秒164次,只寫時每秒68次,同時讀寫時分別為每秒14和30次(波動很大)。此外,這種方式因為最初啟動的幾個線程持續訪問數據庫,后加入的線程會滯后幾秒才啟動,且很難打開數據庫連接或創建prepare statement。調試時發現只會啟用2個線程,但是隨隊列中block數目的增加,讀性能增高,寫性能降低。讀寫各3個block時分別為每秒35和14次。

WAL模式下甚至連初始時啟動2個線程都會被lock,因此只能改成不斷重試:

static void readData() { static const char *query = "SELECT value FROM test WHERE value < ? ORDER BY value DESC LIMIT 1;"; dispatch_async(queue, ^{ sqlite3 *database = openDb(); sqlite3_stmt *stmt; while (sqlite3_prepare_v2(database, query, -1, &stmt, NULL) != SQLITE_OK); while (YES) { sqlite3_bind_int(stmt, 1, arc4random()); int returnCode = sqlite3_step(stmt); if (returnCode == SQLITE_ROW || returnCode == SQLITE_DONE) { ++readCount; } sqlite3_reset(stmt); } sqlite3_finalize(stmt); sqlite3_close(database); }); } static void writeData() { static const char *update = "UPDATE test SET value = ? WHERE id = ?;"; dispatch_async(queue, ^{ sqlite3 *database = openDb(); sqlite3_stmt *stmt; while (sqlite3_prepare_v2(database, update, -1, &stmt, nil) != SQLITE_OK); while (YES) { sqlite3_bind_int(stmt, 1, arc4random()); sqlite3_bind_int(stmt, 2, arc4random() % 1000 + 1); if (sqlite3_step(stmt) == SQLITE_DONE) { ++writeCount; } sqlite3_reset(stmt); } sqlite3_finalize(stmt); sqlite3_close(database); }); }

結果為只讀時平均每秒169次,只寫時每秒246次,同時讀寫時每秒分別為90和57次(波動較大)。並發效率有了顯著提升,但仍不及第二種方式。

第四種方式相當於讓SQLite來維護隊列,只不過SQL的執行是亂序的,因此無法保證事務性。

先恢復全局的database變量,然后配置成串行模式:

sqlite3_config(SQLITE_CONFIG_SERIALIZED);


再是訪問數據庫:

static void readData() { static const char *query = "SELECT value FROM test WHERE value < ? ORDER BY value DESC LIMIT 1;"; dispatch_async(queue, ^{ sqlite3_stmt *stmt; if (sqlite3_prepare_v2(database, query, -1, &stmt, NULL) == SQLITE_OK) { while (YES) { sqlite3_bind_int(stmt, 1, arc4random()); int returnCode = sqlite3_step(stmt); if (returnCode == SQLITE_ROW || returnCode == SQLITE_DONE) { ++readCount; } sqlite3_reset(stmt); } sqlite3_finalize(stmt); } else { NSLog(@"Failed to prepare statement: %s", sqlite3_errmsg(database)); } }); } static void writeData() { static const char *update = "UPDATE test SET value = ? WHERE id = ?;"; dispatch_async(queue, ^{ sqlite3_stmt *stmt; if (sqlite3_prepare_v2(database, update, -1, &stmt, NULL) == SQLITE_OK) { while (YES) { sqlite3_bind_int(stmt, 1, arc4random()); sqlite3_bind_int(stmt, 2, arc4random() % 1000 + 1); if (sqlite3_step(stmt) == SQLITE_DONE) { ++writeCount; } sqlite3_reset(stmt); } sqlite3_finalize(stmt); } else { NSLog(@"Failed to prepare statement: %s", sqlite3_errmsg(database)); } }); }

測試結果為只讀時平均每秒164次,只寫時每秒68次,同時讀寫時每秒分別為57和43次。讀線程比寫線程的速率更高,而且新線程的加入不需要等待。
WAL模式下,只讀時平均每秒176次,只寫時每秒254次,同時讀寫時每秒分別為109和85次。

由此可見,要獲得最好的性能的話,WAL模式是必須啟用的,為此也有必要自己編譯SQLite 3.7.0以上的版本(除非不支持iOS 4.2及以下版本)。
而在測試過的后3種方式中:第3種是效率最低的,不建議使用;第4種讀取性能更高,適合無需使用事務的場合;第2種適用范圍更廣,效率也足夠優秀,一般應采用這種方式。
不過要注意的是,第2種方式在測試時的邏輯是完全與數據庫相關的。實際中可能要做計算或IO訪問等工作,在此期間其他線程都是被阻塞的,這樣就會大大降低效率了。因此只建議把訪問數據庫的邏輯放入隊列,其余工作在其他線程里完成。

剛才洗澡時我又想到一點,既然第2種方式不能並行,第4種方式不能保證事務性,那么能否將各自的優點結合起來呢?
於是一個新的實現方案又浮出水面了:使用2個串行隊列,分別負責讀和寫,每個隊列各使用一個數據庫連接,線程模式可以采用多線程或串行模式。
代碼拿方式2稍做修改就行了,這里就不列出了。測試結果波動比較大(估計是checkpoint的影響),多線程模式下平均約為89和73次,串行模式下為91和86次。
但在iPad 2這種雙核的機型上,多線程明顯要比單隊列更具優勢:方式2的成績是每秒各85次,方式3是94和124次(寫波動較大),方式4是95和72次,而新方案在多線程模式下是104和168次(寫波動很大,40~280之間),串行模式下為108和177次(寫波動很大)。
因此極端的優化情況下,可以根據CPU核心數來創建隊列數,然后把數據庫訪問線程隨機分配到某個隊列中。不過考慮到iOS設備這種嵌入式平台並不需要密集地訪問數據庫,而且除數據庫線程以外還有其他事要做,如果沒遇到瓶頸的話,簡單的方案2其實也夠用了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM