SQLite在多線程環(huán)境下的應(yīng)用
先說下初衷吧,實(shí)際上我經(jīng)??吹接腥吮г筍QLite不支持多線程。而在iOS開發(fā)時(shí),為了不阻塞主線程,數(shù)據(jù)庫(kù)訪問必須移到子線程中。為了解決這個(gè)矛盾,很有必要對(duì)此一探究竟。
關(guān)于這個(gè)問題,最權(quán)威的解答當(dāng)然是SQLite官網(wǎng)上的“Is SQLite threadsafe?”這個(gè)問答。
簡(jiǎn)單來說,從3.3.1版本開始,它就是線程安全的了。而iOS的SQLite版本沒有低于這個(gè)版本的:
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 - iOS 4.1 / 4.2
3.7.2 - iOS 4.3
3.7.7 - iOS 5.0
當(dāng)然,你也可以自己編譯最新版本。只是我發(fā)現(xiàn)自己編譯出來的3.7.8居然比iOS 4.3.3內(nèi)置的3.7.2慢了一半,不知道蘋果做了什么優(yōu)化。發(fā)現(xiàn)是我編譯成了debug版本,改成release后性能比內(nèi)置版本高5%左右,不過構(gòu)建出來的app會(huì)大420k左右。
不過這個(gè)線程安全仍然是有限制的,在這篇《Is SQLite thread-safe?》里有詳細(xì)的解釋。
另一篇重要的文檔就是《SQLite And Multiple Threads》。它指出SQLite支持3種線程模式:
單線程:禁用所有的mutex鎖,并發(fā)使用時(shí)會(huì)出錯(cuò)。當(dāng)SQLite編譯時(shí)加了SQLITE_THREADSAFE=0參數(shù),或者在初始化SQLite前調(diào)用sqlite3_config(SQLITE_CONFIG_SINGLETHREAD)時(shí)啟用。
多線程:只要一個(gè)數(shù)據(jù)庫(kù)連接不被多個(gè)線程同時(shí)使用就是安全的。源碼中是啟用bCoreMutex,禁用bFullMutex。實(shí)際上就是禁用數(shù)據(jù)庫(kù)連接和prepared statement(準(zhǔn)備好的語句)上的鎖,因此不能在多個(gè)線程中并發(fā)使用同一個(gè)數(shù)據(jù)庫(kù)連接或prepared statement。當(dāng)SQLite編譯時(shí)加了SQLITE_THREADSAFE=2參數(shù)時(shí)默認(rèn)啟用。若SQLITE_THREADSAFE不為0,可以在初始化SQLite前,調(diào)用sqlite3_config(SQLITE_CONFIG_MULTITHREAD)啟用;或者在創(chuàng)建數(shù)據(jù)庫(kù)連接時(shí),設(shè)置SQLITE_OPEN_NOMUTEX flag。
串行:?jiǎn)⒂盟械逆i,包括bCoreMutex和bFullMutex。因?yàn)閿?shù)據(jù)庫(kù)連接和prepared statement都已加鎖,所以多線程使用這些對(duì)象時(shí)沒法并發(fā),也就變成串行了。當(dāng)SQLite編譯時(shí)加了SQLITE_THREADSAFE=1參數(shù)時(shí)默認(rèn)啟用。若SQLITE_THREADSAFE不為0,可以在初始化SQLite前,調(diào)用sqlite3_config(SQLITE_CONFIG_SERIALIZED)啟用;或者在創(chuàng)建數(shù)據(jù)庫(kù)連接時(shí),設(shè)置SQLITE_OPEN_FULLMUTEX flag。
而這里所說的初始化是指調(diào)用sqlite3_initialize()函數(shù),這個(gè)函數(shù)在調(diào)用sqlite3_open()時(shí)會(huì)自動(dòng)調(diào)用,且只有第一次調(diào)用是有效的。
另一個(gè)要說明的是prepared statement,它是由數(shù)據(jù)庫(kù)連接(的pager)來管理的,使用它也可看成使用這個(gè)數(shù)據(jù)庫(kù)連接。因此在多線程模式下,并發(fā)對(duì)同一個(gè)數(shù)據(jù)庫(kù)連接調(diào)用sqlite3_prepare_v2()來創(chuàng)建prepared statement,或者對(duì)同一個(gè)數(shù)據(jù)庫(kù)連接的任何prepared statement并發(fā)調(diào)用sqlite3_bind_*()和sqlite3_step()等函數(shù)都會(huì)出錯(cuò)(在iOS上,該線程會(huì)出現(xiàn)EXC_BAD_ACCESS而中止)。這種錯(cuò)誤無關(guān)讀寫,就是只讀也會(huì)出錯(cuò)。文檔中給出的安全使用規(guī)則是:沒有事務(wù)正在等待執(zhí)行,所有prepared statement都被finalized。
順帶一提,調(diào)用sqlite3_threadsafe()可以獲得編譯期的SQLITE_THREADSAFE參數(shù)。標(biāo)準(zhǔn)發(fā)行版是1,也就是串行模式;而iOS上是2,也就是多線程模式;Python的sqlite3模塊也默認(rèn)使用串行模式,可以用sqlite3.threadsafety來配置。但是默認(rèn)情況下,一個(gè)線程只能使用當(dāng)前線程打開的數(shù)據(jù)庫(kù)連接,除非在連接時(shí)設(shè)置了check_same_thread=False參數(shù)。
現(xiàn)在3種模式都有所了解了,清楚SQLite并不是對(duì)多線程無能為力后,接下來就了解下事務(wù)吧。
數(shù)據(jù)庫(kù)只有在事務(wù)中才能被更改。所有更改數(shù)據(jù)庫(kù)的命令(除SELECT以外的所有SQL命令)都會(huì)自動(dòng)開啟一個(gè)新事務(wù),并且當(dāng)最后一個(gè)查詢完成時(shí)自動(dòng)提交。
而BEGIN命令可以手動(dòng)開始事務(wù),并關(guān)閉自動(dòng)提交。當(dāng)下一條COMMIT命令執(zhí)行時(shí),自動(dòng)提交再次打開,事務(wù)中所做的更改也被寫入數(shù)據(jù)庫(kù)。當(dāng)COMMIT失敗時(shí),自動(dòng)提交仍然關(guān)閉,以便讓用戶嘗試再次提交。若執(zhí)行的是ROLLBACK命令,則也打開自動(dòng)提交,但不保存事務(wù)中的更改。關(guān)閉數(shù)據(jù)庫(kù)或遇到錯(cuò)誤時(shí),也會(huì)自動(dòng)回滾事務(wù)。
經(jīng)常有人抱怨SQLite的插入太慢,實(shí)際上它可以做到每秒插入幾萬次,但是每秒只能提交幾十次事務(wù)。因此在插入大批數(shù)據(jù)時(shí),可以通過禁用自動(dòng)提交來提速。
事務(wù)在改寫數(shù)據(jù)庫(kù)文件時(shí),會(huì)先生成一個(gè)rollback journal(回滾日志),記錄初始狀態(tài)(其實(shí)就是備份),所有改動(dòng)都是在數(shù)據(jù)庫(kù)文件上進(jìn)行的。當(dāng)事務(wù)需要回滾時(shí),可以將備份文件的內(nèi)容還原到數(shù)據(jù)庫(kù)文件;提交成功時(shí),默認(rèn)的delete模式下會(huì)直接刪除這個(gè)日志。這個(gè)日志也可以幫助解決事務(wù)執(zhí)行過程中斷電,導(dǎo)致數(shù)據(jù)庫(kù)文件損壞的問題。但如果操作系統(tǒng)或文件系統(tǒng)有bug,或是磁盤損壞,則仍有可能無法恢復(fù)。
而從3.7.0版本(對(duì)應(yīng)iOS 4.3)開始,SQLite還提供了Write-Ahead Logging模式。與delete模式相比,WAL模式在大部分情況下更快,并發(fā)性更好,讀和寫之間互不阻塞;而其缺點(diǎn)對(duì)于iPhone這種嵌入式設(shè)備來說可以忽略,只需注意不要以只讀方式打開WAL模式的數(shù)據(jù)庫(kù)即可。
使用WAL模式時(shí),改寫操是附加(append)到WAL文件,而不改動(dòng)數(shù)據(jù)庫(kù)文件,因此數(shù)據(jù)庫(kù)文件可以被同時(shí)讀取。當(dāng)執(zhí)行checkpoint操作時(shí),WAL文件的內(nèi)容會(huì)被寫回?cái)?shù)據(jù)庫(kù)文件。當(dāng)WAL文件達(dá)到SQLITE_DEFAULT_WAL_AUTOCHECKPOINT(默認(rèn)值是1000)頁(默認(rèn)大小是1KB)時(shí),會(huì)自動(dòng)使用當(dāng)前COMMIT的線程來執(zhí)行checkpoint操作。也可以關(guān)閉自動(dòng)checkpoint,改為手動(dòng)定期checkpoint。
為了避免讀取的數(shù)據(jù)不一致,查詢時(shí)也需要讀取WAL文件,并記錄一個(gè)結(jié)尾標(biāo)記(end mark)。這樣的代價(jià)就是讀取會(huì)變得稍慢,但是寫入會(huì)變快很多。要提高查詢性能的話,可以減小WAL文件的大小,但寫入性能也會(huì)降低。
需要注意的是,低版本的SQLite不能讀取高版本的SQLite生成的WAL文件,但是數(shù)據(jù)庫(kù)文件是通用的。這種情況在用戶進(jìn)行iOS降級(jí)時(shí)可能會(huì)出現(xiàn),可以把模式改成delete,再改回WAL來修復(fù)。
要對(duì)一個(gè)數(shù)據(jù)庫(kù)連接啟用WAL模式,需要執(zhí)行“PRAGMA journal_mode=WAL;”這條命令,它的默認(rèn)值是“journal_mode=DELETE”。執(zhí)行后會(huì)返回新的journal_mode字符串值,即成功時(shí)為"wal",失敗時(shí)為之前的模式(例如"delete")。一旦啟用WAL模式后,數(shù)據(jù)庫(kù)會(huì)保持這個(gè)模式,這樣下次打開數(shù)據(jù)庫(kù)時(shí)仍然是WAL模式。
要停止自動(dòng)checkpoint,可以使用wal_autocheckpoint指令或sqlite3_wal_checkpoint()函數(shù)。手動(dòng)執(zhí)行checkpoint可以使用wal_checkpoint指令或sqlite3_wal_checkpoint()函數(shù)。
還有一個(gè)很重要的知識(shí)點(diǎn)需要強(qiáng)調(diào):事務(wù)是和數(shù)據(jù)庫(kù)連接相關(guān)的,每個(gè)數(shù)據(jù)庫(kù)連接(使用pager來)維護(hù)自己的事務(wù),且同時(shí)只能有一個(gè)事務(wù)(但是可以用SAVEPOINT來實(shí)現(xiàn)內(nèi)嵌事務(wù))。
也就是說,事務(wù)與線程無關(guān),一個(gè)線程里可以同時(shí)用多個(gè)數(shù)據(jù)庫(kù)連接來完成多個(gè)事務(wù),而多個(gè)線程也可以同時(shí)(非并發(fā))使用一個(gè)數(shù)據(jù)庫(kù)連接來共同完成一個(gè)事務(wù)。
下面用Python來演示一下:
- # -*- coding: utf-8 -*-
- import sqlite3
- import threading
- def f():
- con.rollback()
- con = sqlite3.connect('test.db', check_same_thread=False) # 允許在其他線程中使用這個(gè)連接
- 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()
在這個(gè)例子中,雖然是在子線程中執(zhí)行rollback,但由于和主線程用的是同一個(gè)數(shù)據(jù)庫(kù)連接,所以主線程所做的更改也被回滾了。
而如果是用不同的數(shù)據(jù)庫(kù)連接,每個(gè)連接都不能讀取其他連接中未提交的數(shù)據(jù),除非使用read-uncommitted模式。
而要實(shí)現(xiàn)事務(wù),就不得不用到鎖。
一個(gè)SQLite數(shù)據(jù)庫(kù)文件有5種鎖的狀態(tài):
UNLOCKED:表示數(shù)據(jù)庫(kù)此時(shí)并未被讀寫。
SHARED:表示數(shù)據(jù)庫(kù)可以被讀取。SHARED鎖可以同時(shí)被多個(gè)線程擁有。一旦某個(gè)線程持有SHARED鎖,就沒有任何線程可以進(jìn)行寫操作。
RESERVED:表示準(zhǔn)備寫入數(shù)據(jù)庫(kù)。RESERVED鎖最多只能被一個(gè)線程擁有,此后它可以進(jìn)入PENDING狀態(tài)。
PENDING:表示即將寫入數(shù)據(jù)庫(kù),正在等待其他讀線程釋放SHARED鎖。一旦某個(gè)線程持有PENDING鎖,其他線程就不能獲取SHARED鎖。這樣一來,只要等所有讀線程完成,釋放SHARED鎖后,它就可以進(jìn)入EXCLUSIVE狀態(tài)了。
EXCLUSIVE:表示它可以寫入數(shù)據(jù)庫(kù)了。進(jìn)入這個(gè)狀態(tài)后,其他任何線程都不能訪問數(shù)據(jù)庫(kù)文件。因此為了并發(fā)性,它的持有時(shí)間越短越好。
一個(gè)線程只有在擁有低級(jí)別的鎖的時(shí)候,才能獲取更高一級(jí)的鎖。SQLite就是靠這5種類型的鎖,巧妙地實(shí)現(xiàn)了讀寫線程的互斥。同時(shí)也可看出,寫操作必須進(jìn)入EXCLUSIVE狀態(tài),此時(shí)并發(fā)數(shù)被降到1,這也是SQLite被認(rèn)為并發(fā)插入性能不好的原因。
另外,read-uncommitted和WAL模式會(huì)影響這個(gè)鎖的機(jī)制。在這2種模式下,讀線程不會(huì)被寫線程阻塞,即使寫線程持有PENDING或EXCLUSIVE鎖。
提到鎖就不得不說到死鎖的問題,而SQLite也可能出現(xiàn)死鎖。
下面舉個(gè)例子:
連接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)
現(xiàn)在2個(gè)連接都在等待對(duì)方釋放鎖,于是就死鎖了。當(dāng)然,實(shí)際情況并沒那么糟糕,任何一方選擇不繼續(xù)等待,回滾事務(wù)就行了。
不過要更好地解決這個(gè)問題,就必須更深入地了解事務(wù)了。
實(shí)際上BEGIN語句可以有3種起始狀態(tài):
DEFERRED:默認(rèn)值,開始事務(wù)時(shí)不獲取任何鎖。進(jìn)行第一次讀操作時(shí)獲取SHARED鎖,進(jìn)行第一次寫操作時(shí)獲取RESERVED鎖。
IMMEDIATE:開始事務(wù)時(shí)獲取RESERVED鎖。
EXCLUSIVE:開始事務(wù)時(shí)獲取EXCLUSIVE鎖。
現(xiàn)在考慮2個(gè)事務(wù)在開始時(shí)都使用IMMEDIATE方式:
連接1:BEGIN IMMEDIATE (RESERVED)
連接1:SELECT ... (RESERVED)
連接1:INSERT ... (RESERVED)
連接2:BEGIN IMMEDIATE (嘗試獲取RESERVED鎖,但已有RESERVED鎖未釋放,因此事務(wù)開始失敗,返回SQLITE_BUSY,等待用戶重試)
連接1:COMMIT (EXCLUSIVE,寫入完成后釋放)
連接2:BEGIN IMMEDIATE (RESERVED)
連接2:SELECT ... (RESERVED)
連接2:INSERT ... (RESERVED)
連接2:COMMIT (EXCLUSIVE,寫入完成后釋放)
這樣死鎖就被避免了。
而EXCLUSIVE方式則更為嚴(yán)苛,即使其他連接以DEFERRED方式開啟事務(wù)也不會(huì)死鎖:
連接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狀態(tài)長(zhǎng)期阻塞其他請(qǐng)求,最好的方式還是讓所有寫事務(wù)都以IMMEDIATE方式開始。
順帶一提,要實(shí)現(xiàn)重試的話,可以使用sqlite3_busy_timeout()或sqlite3_busy_handler()函數(shù)。
由此可見,要想保證線程安全的話,可以有這4種方式:
SQLite使用單線程模式,用一個(gè)專門的線程訪問數(shù)據(jù)庫(kù)。
SQLite使用單線程模式,用一個(gè)線程隊(duì)列來訪問數(shù)據(jù)庫(kù),隊(duì)列一次只允許一個(gè)線程執(zhí)行,隊(duì)列里的線程共用一個(gè)數(shù)據(jù)庫(kù)連接。
SQLite使用多線程模式,每個(gè)線程創(chuàng)建自己的數(shù)據(jù)庫(kù)連接。
SQLite使用串行模式,所有線程共用全局的數(shù)據(jù)庫(kù)連接。
接下來就一一測(cè)試這幾種方式在iPhone 4(iOS 4.3.3,SQLite 3.7.2)上的性能表現(xiàn)。
第一種方式太過麻煩,需要線程間通信,這里我就忽略了。
第二種方式可以用dispatch_queue_create()來創(chuàng)建一個(gè)serial queue,或者用一個(gè)maxConcurrentOperationCount為1的NSOperationQueue來實(shí)現(xiàn)。
這種方式的缺點(diǎn)就是事務(wù)必須在一個(gè)block或operation里完成,否則會(huì)亂序;而耗時(shí)較長(zhǎng)的事務(wù)會(huì)阻塞隊(duì)列。另外,沒法利用多核CPU的優(yōu)勢(shì)。
先初始化數(shù)據(jù)庫(kù):
- #import
- 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條測(cè)試數(shù)據(jù):
- 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);
- }
- }
然后創(chuàng)建一個(gè)串行隊(duì)列:
- static dispatch_queue_t queue;
- - (void)viewDidLoad {
- // ...
- queue = dispatch_queue_create("net.keakon.db", NULL);
- }
再設(shè)置一個(gè)計(jì)數(shù)器,每秒執(zhí)行一次:
- 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];
- }
這樣就可以開始測(cè)試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()來異步地遞歸調(diào)用block。
因?yàn)閎lock是在棧里生成的,異步執(zhí)行時(shí)已經(jīng)被銷毀,所以需要copy到堆。因?yàn)樾枰恢眻?zhí)行,所以我就沒release了。
此外,光copy的話還是無法正常執(zhí)行,但是把block本身的存儲(chǔ)類型設(shè)為__block后就正常了,原因我也不清楚。
測(cè)試結(jié)果為只讀時(shí)平均每秒165次,只寫時(shí)每秒68次,同時(shí)讀寫時(shí)每秒各47次。換成多線程或串行模式時(shí),效率也差不多。
接著試試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); // 每次測(cè)試前先checkpoint,避免WAL文件過大而影響性能
測(cè)試結(jié)果為只讀時(shí)平均每秒166次,只寫時(shí)每秒244次,同時(shí)讀寫時(shí)每秒各97次。并發(fā)性增加了1倍有木有!更夸張的是寫入比讀取還快了。
在自編譯的3.7.8版中,同時(shí)讀寫為每秒各102次,加上SQLITE_THREADSAFE=0參數(shù)后為每秒各104次,性能稍有提升。
第三種方式需要打開和關(guān)閉數(shù)據(jù)庫(kù)連接,所以會(huì)額外消耗一些時(shí)間。此外還要維持各個(gè)連接間的互斥,事務(wù)也比較容易沖突,但能確保事務(wù)正確執(zhí)行。
首先需要移除全局的database變量,并修改openDb()函數(shù):
- 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);
隊(duì)列改成可以亂序執(zhí)行的:
- queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
然后是訪問數(shù)據(jù)庫(kù):
- 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);
- });
- }
這里就無需遞歸調(diào)用了,直接在子線程中循環(huán)即可。
測(cè)試結(jié)果為只讀時(shí)平均每秒164次,只寫時(shí)每秒68次,同時(shí)讀寫時(shí)分別為每秒14和30次(波動(dòng)很大)。此外,這種方式因?yàn)樽畛鯁?dòng)的幾個(gè)線程持續(xù)訪問數(shù)據(jù)庫(kù),后加入的線程會(huì)滯后幾秒才啟動(dòng),且很難打開數(shù)據(jù)庫(kù)連接或創(chuàng)建prepare statement。調(diào)試時(shí)發(fā)現(xiàn)只會(huì)啟用2個(gè)線程,但是隨隊(duì)列中block數(shù)目的增加,讀性能增高,寫性能降低。讀寫各3個(gè)block時(shí)分別為每秒35和14次。
WAL模式下甚至連初始時(shí)啟動(dòng)2個(gè)線程都會(huì)被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);
- });
- }
結(jié)果為只讀時(shí)平均每秒169次,只寫時(shí)每秒246次,同時(shí)讀寫時(shí)每秒分別為90和57次(波動(dòng)較大)。并發(fā)效率有了顯著提升,但仍不及第二種方式。
第四種方式相當(dāng)于讓SQLite來維護(hù)隊(duì)列,只不過SQL的執(zhí)行是亂序的,因此無法保證事務(wù)性。
先恢復(fù)全局的database變量,然后配置成串行模式:
sqlite3_config(SQLITE_CONFIG_SERIALIZED);
再是訪問數(shù)據(jù)庫(kù):
- 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));
- }
- });
- }
測(cè)試結(jié)果為只讀時(shí)平均每秒164次,只寫時(shí)每秒68次,同時(shí)讀寫時(shí)每秒分別為57和43次。讀線程比寫線程的速率更高,而且新線程的加入不需要等待。
WAL模式下,只讀時(shí)平均每秒176次,只寫時(shí)每秒254次,同時(shí)讀寫時(shí)每秒分別為109和85次。
由此可見,要獲得最好的性能的話,WAL模式是必須啟用的,為此也有必要自己編譯SQLite 3.7.0以上的版本(除非不支持iOS 4.2及以下版本)。
而在測(cè)試過的后3種方式中:第3種是效率最低的,不建議使用;第4種讀取性能更高,適合無需使用事務(wù)的場(chǎng)合;第2種適用范圍更廣,效率也足夠優(yōu)秀,一般應(yīng)采用這種方式。
不過要注意的是,第2種方式在測(cè)試時(shí)的邏輯是完全與數(shù)據(jù)庫(kù)相關(guān)的。實(shí)際中可能要做計(jì)算或IO訪問等工作,在此期間其他線程都是被阻塞的,這樣就會(huì)大大降低效率了。因此只建議把訪問數(shù)據(jù)庫(kù)的邏輯放入隊(duì)列,其余工作在其他線程里完成。
剛才洗澡時(shí)我又想到一點(diǎn),既然第2種方式不能并行,第4種方式不能保證事務(wù)性,那么能否將各自的優(yōu)點(diǎn)結(jié)合起來呢?
于是一個(gè)新的實(shí)現(xiàn)方案又浮出水面了:使用2個(gè)串行隊(duì)列,分別負(fù)責(zé)讀和寫,每個(gè)隊(duì)列各使用一個(gè)數(shù)據(jù)庫(kù)連接,線程模式可以采用多線程或串行模式。
代碼拿方式2稍做修改就行了,這里就不列出了。測(cè)試結(jié)果波動(dòng)比較大(估計(jì)是checkpoint的影響),多線程模式下平均約為89和73次,串行模式下為91和86次。
但在iPad 2這種雙核的機(jī)型上,多線程明顯要比單隊(duì)列更具優(yōu)勢(shì):方式2的成績(jī)是每秒各85次,方式3是94和124次(寫波動(dòng)較大),方式4是95和72次,而新方案在多線程模式下是104和168次(寫波動(dòng)很大,40~280之間),串行模式下為108和177次(寫波動(dòng)很大)。
因此極端的優(yōu)化情況下,可以根據(jù)CPU核心數(shù)來創(chuàng)建隊(duì)列數(shù),然后把數(shù)據(jù)庫(kù)訪問線程隨機(jī)分配到某個(gè)隊(duì)列中。不過考慮到iOS設(shè)備這種嵌入式平臺(tái)并不需要密集地訪問數(shù)據(jù)庫(kù),而且除數(shù)據(jù)庫(kù)線程以外還有其他事要做,如果沒遇到瓶頸的話,簡(jiǎn)單的方案2其實(shí)也夠用了。