iOS sqlite3數據庫解析


看來從版本3.3.1基本上已經支持線程句柄的傳遞功能。具體限制我標記了一下。
(6) Is SQLite threadsafe?
SQLite is threadsafe. We make this concession since many users choose to ignore the advice given in the previous paragraph. But in order to be thread-safe, SQLite must be compiled with the SQLITE_THREADSAFE preprocessor macro set to 1. Both the Windows and linux precompiled binaries in the distribution are compiled this way. If you are unsure if the SQLite library you are linking against is compiled to be threadsafe you can call the sqlite3_threadsafe() interface to find out.

Prior to version 3.3.1, an sqlite3 structure could only be used in the same thread that called sqlite3_open() to create it. You could not open a database in one thread then pass the handle off to another thread for it to use. This was due to limitations (bugs?) in many common threading implementations such as on RedHat9. Specifically, an fcntl() lock created by one thread cannot be removed or modified by a different thread on the troublesome systems. And since SQLite uses fcntl() locks heavily for concurrency control, serious problems arose if you start moving database connections across threads.

The restriction on moving database connections across threads was relaxed somewhat in version 3.3.1. With that and subsequent versions, it is safe to move a connection handle across threads as long as the connection is not holding any fcntl() locks. 

You can safely assume that no locks are being held if no transaction is pending and all statements have been finalized.
只要不在事務過程當中,並且所有的statements已經被finalized,那么不同線程間共享句柄就是安全的.

Under UNIX, you should not carry an open SQLite database across a fork() system call into the child process. Problems will result if you do.


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

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

 

這幾天研究了一下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其實也夠用了。

 

1)如果數據庫配置成為串行模式:sqlite3_config(SQLITE_CONFIG_SERIALIZED)

     此模式下可以多個線程使用一個數據庫連接,但顯然對數據庫的操作很慢;

     所以建議:

     數據庫配置成多線程模式:sqlite3_config(SQLITE_CONFIG_MULTITHREAD)

     此模式下多個線程必須各自使用自己的數據庫連接,多個連接對SQLite的操作不用擔心,因為SQLite是線程安全的。

     但是這樣仍然不好,線程數過多(上百左右個線程發起數據操作)會導致數據庫壓力大,尤其是有事務操作時會發起很多事務請求,這時所有的帶事務請求都會卡在beginTransaction,嚴重影響性能,使得整個App中所有對數據庫數據有請求的頁面一直loading,雖然界面不會卡死,但是數據一直出不來;

     所以,采用FMDB queue(其實就是GCD串行隊列),因為FMDB queue串行隊列,所以所有的數據操作包括事務都是串行執行的,所有多線程發起的數據庫操作都被異步放到了這個隊列里,不會卡住UI,也不會有數據庫事務並發問題;

2)事務中有大數據量(幾千條)條件查詢(注意是條件查詢),然后再數據更新和插入

     按1)優化后也有可能性能還是慢的情況,也就是如2)題,這種查詢特別慢,嚴重影響了事務的執行;所以盡量避免在事務中有大數據量條件查詢;

3)最后,把數據庫的WAL模式開啟,這樣數據庫的寫操作很快,而且可以對數據並發寫和讀取;

     WAL is significantly faster in most scenarios.

     WAL provides more concurrency as readers do not block writers and a writer does not block readers. Reading and writing can proceed concurrently.

     Disk I/O operations tends to be more sequential using WAL.

     WAL uses many fewer fsync() operations and is thus less vulnerable to problems on systems where the fsync() system call is broken.

 

綜上,總結如下:

1)數據庫多線程模下,有並發事務或並發事務多會超成數據庫操作擁堵,這個與數據庫模式無關(FMDB queue解決)

2)事務中有大數據(幾千條)的先條件查詢后更新或插入操作時會很慢(避免事務中大數據條件查詢)

2)開啟WAL模式;

3)區分對待數據庫串行模式和串行數據庫操作隊列;

     目前,我采用的是:a)多線程數據庫操作+FMDB Queue(W/R兩隊表兩數據庫連接)+數據庫SQLITE_CONFIG_MULTITHREAD模式+WAL+事務中避免大數據條件查詢

     但是,我覺得另一種方式的性能也應該不錯:b)多線程數據庫操作+數據庫SQLITE_CONFIG_SERIALIZED模式+(WAL)+一個數據庫連接+事務中避免大數據條件查詢

 

     a)中數據庫模式雖然是多線程(SQLITE_CONFIG_MULTITHREAD)模式,但所有程序級多線程數據庫操作都被FMDB queue串行化,最多也就兩個數據庫並發(W/R),各自一個數據庫連接; SQLITE_CONFIG_MULTITHREAD模式是好處在於讀和寫可分成各一個FMDB queue以及開啟WAL模式支持可並發寫和讀

     b)中數據庫模式是串行(SQLITE_CONFIG_SERIALIZED)模式且一個數據庫連接,所以不需要隊列來串行化程序級的多線程數據庫操作;同時WAL模式是可選開啟,如果開啟則寫數據庫會更快,但沒有並發的讀和寫(因為當前數據庫是SQLITE_CONFIG_SERIALIZED模式)

 

sqlite3中的常見問題

 

sqlite常見問題

(1) 如何建立自動增長字段?

簡短回答:聲明為 INTEGER PRIMARY KEY 的列將會自動增長。

長一點的答案: 如果你聲明表的一列為 INTEGER PRIMARY KEY,那么, 每當你在該列上插入一NULL值時, NULL自動被轉換為一個比該列中最大值大1的一個整數,如果表是空的, 將會是1。 (如果是最大可能的主鍵 9223372036854775807,那個,將鍵值將是隨機未使用的數。) 如,有下列表:

CREATE TABLE t1(
a INTEGER PRIMARY KEY,
b INTEGER
);

在該表上,下列語句

INSERT INTO t1 VALUES(NULL,123);

在邏輯上等價於:

INSERT INTO t1 VALUES((SELECT max(a) FROM t1)+1,123);

有一個新的API叫做 sqlite3_last_insert_rowid(), 它將返回最近插入的整數值。

注意該整數會比表中該列上的插入之前的最大值大1。 該鍵值在當前的表中是唯一的。但有可能與已從表中刪除的值重疊。 要想建立在整個表的生命周期中唯一的鍵值,需要在 INTEGER PRIMARY KEY 上增加AUTOINCREMENT聲明。那么,新的鍵值將會比該表中曾能存在過的最大值大1。 如果最大可能的整數值在數據表中曾經存在過,INSERT將會失敗, 並返回SQLITE_FULL錯誤代碼。


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

(2)SQLite3支持何種數據類型?

NULL
INTEGER
REAL
TEXT
BLOB
但實際上,sqlite3也接受如下的數據類型:
smallint 16 位元的整數。
interger 32 位元的整數。
decimal(p,s) p 精確值和 s 大小的十進位整數,精確值p是指全部有幾個數(digits)大小值 ,s是指小數點後有幾位數。如果沒有特別指定,則系統會設為 p=5; s=0 。
float 32位元的實數。
double 64位元的實數。
char(n) n 長度的字串,n不能超過 254。
varchar(n) 長度不固定且其最大長度為 n 的字串,n不能超過 4000。
graphic(n) 和 char(n) 一樣,不過其單位是兩個字元 double-bytes, n不能超過127。 這個形態是為了支援兩個字元長度的字體,例如中文字。
vargraphic(n) 可變長度且其最大長度為 n 的雙字元字串,n不能超過 2000。
date 包含了 年份、月份、日期。
time 包含了 小時、分鍾、秒。
timestamp 包含了 年、月、日、時、分、秒、千分之一秒。

參見 http://www.sqlite.org/datatype3.html.
--------------------------------------------------------------------------------

(3)SQLite允許向一個integer型字段中插入字符串!

這是一個特性,而不是一個bug。SQLite不強制數據類型約束。 任何數據都可以插入任何列。你可以向一個整型列中插入任意長度的字符串, 向布爾型列中插入浮點數,或者向字符型列中插入日期型值。 在 CREATE TABLE 中所指定的數據類型不會限制在該列中插入任何數據。 任何列均可接受任意長度的字符串(只有一種情況除外: 標志為INTEGER PRIMARY KEY的列只能存儲64位整數, 當向這種列中插數據除整數以外的數據時,將會產生錯誤。

但SQLite確實使用聲明的列類型來指示你所期望的格式。所以, 例如你向一個整型列中插入字符串時,SQLite會試圖將該字符串轉換成一個整數。 如果可以轉換,它將插入該整數;否則,將插入字符串。 這種特性有時被稱為 類型或列親和性(type or column affinity).


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

(4)為什么SQLite不允許在同一個表不同的兩行上使用0和0.0作主鍵?

主鍵必須是數值類型,將主鍵改為TEXT型將不起作用。

每一行必須有一個唯一的主鍵。對於一個數值型列, SQLite認為 '0' 和 '0.0' 是相同的, 因為他們在作為整數比較時是相等的(參見上一問題)。 所以,這樣值就不唯一了。


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

(5)多個應用程序或一個應用程序的多個實例可以同時訪問同一個數據庫文件嗎?

多個進程可同時打開同一個數據庫。多個進程可以同時進行SELECT 操作,但在任一時刻,只能有一個進程對數據庫進行更改。

SQLite使用讀、寫鎖控制對數據庫的訪問。(在Win95/98/ME等不支持讀、 寫鎖的系統下,使用一個概率性的模擬來代替。)但使用時要注意: 如果數據庫文件存放於一個NFS文件系統上,這種鎖機制可能不能正常工作。 這是因為 fcntl() 文件鎖在很多NFS上沒有正確的實現。 在可能有多個進程同時訪問數據庫的時候,應該避免將數據庫文件放到NFS上。 在Windows上,Microsoft的文檔中說:如果使用 FAT 文件系統而沒有運行 share.exe 守護進程,那么鎖可能是不能正常使用的。那些在Windows上有很多經驗的人告訴我: 對於網絡文件,文件鎖的實現有好多Bug,是靠不住的。如果他們說的是對的, 那么在兩台或多台Windows機器間共享數據庫可能會引起不期望的問題。

我們意識到,沒有其它嵌入式的 SQL 數據庫引擎能象 SQLite 這樣處理如此多的並發。SQLite允許多個進程同時打開一個數據庫, 同時讀一個數據庫。當有任何進程想要寫時,它必須在更新過程中鎖住數據庫文件。 但那通常只是幾毫秒的時間。其它進程只需等待寫進程干完活結束。 典型地,其它嵌入式的SQL數據庫引擎同時只允許一個進程連接到數據庫。

但是,Client/Server數據庫引擎(如 PostgreSQL, MySQL, 或 Oracle) 通常支持更高級別的並發,並且允許多個進程同時寫同一個數據庫。 這種機制在Client/Server結構的數據庫上是可能的, 因為總是有一個單一的服務器進程很好地控制、協調對數據庫的訪問。 如果你的應用程序需要很多的並發,那么你應該考慮使用一個Client/Server 結構的數據庫。但經驗表明,很多應用程序需要的並發,往往比其設計者所想象的少得多。

當SQLite試圖訪問一個被其它進程鎖住的文件時,缺省的行為是返回 SQLITE_BUSY。 可以在C代碼中使用 sqlite3_busy_handler() 或 sqlite3_busy_timeout() API 函數調整這一行為。
---------------------------------------------------------------------------

(6)SQLite線程安全嗎?

線程是魔鬼(Threads are evil)。 避免使用它們。

SQLite 是線程安全的。由於很多用戶會忽略我們在上一段中給出的建議, 我們做出了這種讓步。但是,為了達到線程安全,SQLite在編譯時必須將 SQLITE_THREADSAFE 預處理宏置為1。在Windows和Linux上, 已編譯的好的二進制發行版中都是這樣設置的。 如果不確定你所使用的庫是否是線程安全的,可以調用 sqlite3_threadsafe() 接口找出。

在 3.3.1 版本之前,一個 sqlite3 結構只能被用於調用 sqlite3_open 創建的同一線程。你不能在一個線程中打開數據庫, 然后將數據庫句柄傳遞給另外一個進程使用。 這主要是由於在好多通用的線程實現(如RedHat9)中的限制引起的(是Bug嗎?)。 特別的,在有問題的系統上,一個進程創建的 fcntl() 鎖無法被其它線程清除或修改。 所以,由於 SQLite 大量使用 fcntl() 鎖做並發控制, 如果你在不同的線程間移動數據庫連接,就可能會出現嚴重的問題。

在3.3.1版本上,關於在線程間移動數據庫連接的限制變得寬松了。 在它及以后的版本中,只要連接沒有持有 fcntl() 鎖,在線程間移動句柄是安全的。 如果沒有未決的事務,並且所有的語句都已執行完畢, 你就可以安全的假定不再持有任何鎖。

在UNIX中,在執行 fork() 系統調用時不應攜帶已打開的數據庫進入子進程。 那樣做將會有問題。


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

(7)在SQLite數據庫中如何列出所有的表和索引?

如果你運行 sqlite3 命令行來訪問你的數據庫,可以鍵入 “.tables”來獲得所有表的列表。或者,你可以輸入 “.schema” 來看整個數據庫模式,包括所有的表的索引。 輸入這些命令,后面跟一個LIKE模式匹配可以限制顯示的表。

在一個 C/C++ 程序中(或者腳本語言使用 Tcl/Ruby/Perl/Python 等) 你可以在一個特殊的名叫 SQLITE_MASTER 上執行一個SELECT查詢以獲得所有 表的索引。每一個 SQLite 數據庫都有一個叫 SQLITE_MASTER 的表, 它定義數據庫的模式。 SQLITE_MASTER 表看起來如下:

CREATE TABLE sqlite_master (
type TEXT,
name TEXT,
tbl_name TEXT,
rootpage INTEGER,
sql TEXT
);

對於表來說,type 字段永遠是 'table',name 字段永遠是表的名字。所以,要獲得數據庫中所有表的列表, 使用下列SELECT語句:

SELECT name FROM sqlite_master
WHERE type='table'
ORDER BY name;

對於索引,type 等於 'index', name 則是索引的名字,tbl_name 是該索引所屬的表的名字。 不管是表還是索引,sql 字段是原先用 CREATE TABLE 或 CREATE INDEX 語句創建它們時的命令文本。對於自動創建的索引(用來實現 PRIMARY KEY 或 UNIQUE 約束),sql字段為NULL。

SQLITE_MASTER 表是只讀的。不能對它使用 UPDATE、INSERT 或 DELETE。 它會被 CREATE TABLE、CREATE INDEX、DROP TABLE 和 DROP INDEX 命令自動更新。

臨時表不會出現在 SQLITE_MASTER 表中。臨時表及其索引和觸發器存放在另外一個叫 SQLITE_TEMP_MASTER 的表中。SQLITE_TEMP_MASTER 跟 SQLITE_MASTER 差不多, 但它只是對於創建那些臨時表的應用可見。如果要獲得所有表的列表, 不管是永久的還是臨時的,可以使用類似下面的命令:

SELECT name FROM
(SELECT * FROM sqlite_master UNION ALL
SELECT * FROM sqlite_temp_master)
WHERE type='table'
ORDER BY name


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

(8) SQLite數據庫有已知的大小限制嗎?

在 Windows 和 Unix 下,版本 2.7.4的 SQLite 可以達到 2的41次方字節 (2T 字節)。老版本的為 2的31 次方字節(2G 字節)。
SQLite 版本 2.8 限制一個記錄的容量為 1M。SQLite 版本 3.0 則對單個記錄容量沒有限制。
表名、索引表名、視圖名、觸發器名和字段名沒有長度限制。但 SQL 函數的名稱 (由 sqlite3_create_function() API 函數創建) 不得超過 255 個字符。

對有關SQLite限制的詳細討論,見 limits.html 。


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

(9) 在SQLite中,VARCHAR字段最長是多少?

SQLite 不強制 VARCHAR 的長度。 你可以在 SQLITE 中聲明一個 VARCHAR(10),SQLite還是可以很高興地允許你放入500個字符。 並且這500個字符是原封不動的,它永遠不會被截斷。


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

(10) SQLite支持二進制大對象嗎?

SQLite 3.0 及以后版本允許你在任何列中存儲 BLOB 數據。 即使該列被聲明為其它類型也可以。


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

(11) 在SQLite中,如何在一個表上添加或刪除一列?

SQLite 有有限地 ALTER TABLE 支持。你可以使用它來在表的末尾增加一列,可更改表的名稱。 如果需要對表結構做更復雜的改變,則必須重新建表。 重建時可以先將已存在的數據放到一個臨時表中,刪除原表, 創建新表,然后將數據從臨時表中復制回來。

如,假設有一個 t1 表,其中有 "a", "b", "c" 三列, 如果要刪除列 c ,以下過程描述如何做:

BEGIN TRANSACTION;
CREATE TEMPORARY TABLE t1_backup(a,b);
INSERT INTO t1_backup SELECT a,b FROM t1;
DROP TABLE t1;
CREATE TABLE t1(a,b);
INSERT INTO t1 SELECT a,b FROM t1_backup;
DROP TABLE t1_backup;
COMMIT;


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

(12) 我在數據庫中刪除了很多數據,但數據庫文件沒有變小,是Bug嗎?

不是。當你從SQLite數據庫中刪除數據時, 未用的磁盤空間將會加入一個內部的“自由列表”中。 當你下次插入數據時,這部分空間可以重用。磁盤空間不會丟失, 但也不會返還給操作系統。

如果刪除了大量數據,而又想縮小數據庫文件占用的空間,執行 VACUUM 命令。 VACUUM 將會從頭重新組織數據庫。這將會使用數據庫有一個空的“自由鏈表”, 數據庫文件也會最小。但要注意的是,VACUUM 的執行會需要一些時間 (在SQLite開發時,在Linux上,大約每M字節需要半秒種),並且, 執行過程中需要原數據庫文件至多兩倍的臨時磁盤空間。

對於 SQLite 3.1版本,一個 auto-vacumm 模式可以替代 VACUUM 命令。 可以使用 auto_vacuum pragma 打開。


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

(13) 我可以在商業產品中使用SQLite而不需支付許可費用嗎?

是的。SQLite 在 public domain。 對代碼的任何部分沒有任何所有權聲明。你可以使用它做任何事。


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

(14) 如何在字符串中使用單引號(')?

SQL 標准規定,在字符串中,單引號需要使用逃逸字符,即在一行中使用兩個單引號。在這方面 SQL 用起來類似 Pascal 語言。 SQLite 尊循標准。如:

INSERT INTO xyz VALUES('5 O''clock');


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

(15) SQLITE_SCHEMA error是什么錯誤?為什么會出現該錯誤?

當一個准備好的(prepared)SQL語句不再有效或者無法執行時, 將返回一個 SQLITE_SCHEMA 錯誤。發生該錯誤時,SQL語句必須使用 sqlite3_prepare() API來重新編譯. 在 SQLite 3 中, 一個 SQLITE_SCHEMA 錯誤只會發生在用 sqlite3_prepare()/sqlite3_step()/sqlite3_finalize() API 執行 SQL 時。而不會發生在使用 sqlite3_exec()時。 在版本2中不是這樣。

准備好的語句失效的最通常原因是:在語句准備好后, 數據庫的模式又被修改了。另外的原因會發生在:

數據庫離線:DETACHed.
數據庫被 VACUUMed
一個用戶存儲過程定義被刪除或改變。
一個 collation 序列定義被刪除或改變。
認證函數被改變。
在所有情況下,解決方法是重新編譯並執行該SQL語句。 因為一個已准備好的語句可以由於其它進程改變數據庫模式而失效, 所有使用 sqlite3_prepare()/sqlite3_step()/sqlite3_finalize() API 的代碼都應准備處理 SQLITE_SCHEMA 錯誤。下面給出一個例子:

int rc;
sqlite3_stmt *pStmt;
char zSql[] = "SELECT .....";

do {

sqlite3_prepare(pDb, zSql, -1, &pStmt, 0);

while( SQLITE_ROW==sqlite3_step(pStmt) ){

}


rc = sqlite3_finalize(pStmt);
} while( rc==SQLITE_SCHEMA );

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

(16) 為什么 ROUND(9.95,1) 返回 9.9 而不是 10.0? 9.95不應該圓整 (四舍五入)嗎?

SQLite 使用二進制算術,在二進制中, 無法用有限的二進制位數表示 9.95 。使用 64-bit IEEE 浮點 (SQLite就是使用這個)最接近 9.95 的二進制表示是 9.949999999999999289457264239899814128875732421875。 所在,當你輸入 9.95 時,SQLite實際上以為是上面的數字, 在四舍五入時會舍去。

這種問題在使用二進制浮點數的任何時候都會出現。 通常的規則是記住很多有限的十進制小數都沒有一個對應的二進制表示。 所以,它們只能使用最接近的二進制數。它們通常非常接近, 但也會有些微小的不同,有些時候也會導致你所期望的不同的結果。

 

 

SQLITE的鎖

在SQLite中,鎖和事務是緊密聯系的。為了有效地使用事務,需要了解一些關於如何加鎖的知識。 
SQLite采用粗放型的鎖。當一個連接要寫數據庫,所有其它的連接被鎖住,直到寫連接結束了它的事務。SQLite有一個加鎖表,來幫助不同的寫數據庫都能夠在最后一刻再加鎖, 
以保證最大的並發性。 
SQLite使用鎖逐步上升機制,為了寫數據庫,連接需要逐級地獲得排它鎖。SQLite有5個不同的鎖狀態:未加鎖(UNLOCKED)、共享(SHARED)、保留(RESERVED)、未決(PENDING)和排它(EXCLUSIVE)。每個數據庫連接在同一時刻只能處於其中一個狀態。每種狀態(未加 
鎖狀態除外)都有一種鎖與之對應。 
最初的狀態是未加鎖狀態,在此狀態下,連接還沒有存取數據庫。當連接到了一個數據庫,甚至已經用BEGIN開始了一個事務時,連接都還處於未加鎖狀態。未加鎖狀態的下一個狀態是共享狀態。為了能夠從數據庫中讀(不寫)數據,連接必須首先進入共享狀態,也就是說首先要獲得一個共享鎖。多個連接可以同時獲得並保持共享鎖,也就是說多個連接可以同時從同一個數據庫中讀數據。但哪怕只有一個共享鎖還沒有釋放,也不允許任何連接寫數據庫。

如果一個連接想要寫數據庫,它必須首先獲得一個保留鎖。一個數據庫上同時只能有一個保 留鎖。保留鎖可以與共享鎖共存,保留鎖是寫數據庫的第1階段。保留鎖即不阻止其它擁有 共享鎖的連接繼續讀數據庫,也不阻止其它連接獲得新的共享鎖。 一旦一個連接獲得了保留鎖,它就可以開始處理數據庫修改操作了,盡管這些修改只能在 
緩沖區中進行,而不是實際地寫到磁盤。對讀出內容所做的修改保存在內存緩沖區中。 當連接想要提交修改(或事務)時,需要將保留鎖提升為排它鎖。為了得到排它鎖,還必須首 
先將保留鎖提升為未決鎖。獲得未決鎖之后,其它連接就不能再獲得新的共享鎖了,但已經擁有共享鎖的連接仍然可以繼續正常讀數據庫。此時,擁有未決鎖的連接等待其它擁有共享鎖的連接完成工作並釋放其共享鎖。 
一旦所有其它共享鎖都被釋放,擁有未決鎖的連接就可以將其鎖提升至排它鎖,此時就可以自由地對數據庫進行修改了。所有以前對緩沖區所做的修改都會被寫到數據庫文件。

 

事務的種類

SQLite有三種不同的事務,使用不同的鎖狀態。事務可以開始於:DEFERRED、MMEDIATE或EXCLUSIVE。事務類型在BEGIN命令中指定: 
BEGIN [ DEFERRED | IMMEDIATE | EXCLUSIVE ] TRANSACTION; 
一個DEFERRED事務不獲取任何鎖(直到它需要鎖的時候),BEGIN語句本身也不會做什么事情——它開始於UNLOCK狀態。默認情況下就是這樣的,如果僅僅用BEGIN開始一個事 
務,那么事務就是DEFERRED的,同時它不會獲取任何鎖;當對數據庫進行第一次讀操作時,它會獲取SHARED鎖;同樣,當進行第一次寫操作時,它會獲取RESERVED鎖。由BEGIN開始的IMMEDIATE 事務會嘗試獲取RESERVED鎖。如果成功,BEGIN IMMEDIATE保證沒有別的連接可以寫數據庫。但是,別的連接可以對數據庫進行讀操作;但是,RESERVED鎖會阻止其它連接的BEGIN IMMEDIATE或者BEGIN EXCLUSIVE命令,當其它連接執行上述命令時,會返回SQLITE_BUSY錯誤。這時你就可以對數據庫進行 
修改操作了,但是你還不能提交,當你COMMIT時,會返回SQLITE_BUSY錯誤,這意味着還有其它的讀事務沒有完成,得等它們執行完后才能提交事務。

EXCLUSIVE事務會試着獲取對數據庫的EXCLUSIVE鎖。這與IMMEDIATE類似,但是一旦成功,EXCLUSIVE事務保證沒有其它的連接,所以就可對數據庫進行讀寫操作了。 
上節那個例子的問題在於兩個連接最終都想寫數據庫,但是它們都沒有放棄各自原來的鎖最終,SHARED鎖導致了問題的出現。如果兩個連接都以BEGIN IMMEDIATE開始事務, 
那么死鎖就不會發生。在這種情況下,在同一時刻只能有一個連接進入BEGIN IMMEDIATE,其它的連接就得等待。BEGIN IMMEDIATE和BEGIN EXCLUSIVE通常被寫 
事務使用。就像同步機制一樣,它防止了死鎖的產生。基本的准則是:如果你正在使用的數據庫沒有其它的連接,用BEGIN就足夠了。但是,如果你使用的數據庫有其它的連接也會對數據庫進行寫操作,就得使用BEGIN IMMEDIATE或BEGIN EXCLUSIVE開始你的事務。

 

事務的生命周期

有一些關於代碼和事務的問題需要關注。首先需要知道哪個對象運行在哪個事務之下。另一個問題是持續時間——一個事務何時開始,何時結束,從哪一點開始影響其它連接?第一 
個問題與API直接關聯,第二個與SQL的一般概念及SQLite的特殊實現關聯。 
一個連接(connection)可以包含多個語句(statement),而且每個連接有一個與數據庫關聯的B-tree和一個pager。Pager在連接中起着很重要的作用,因為它管理事務、鎖、內存緩沖以及負責崩潰恢復(crash recovery)。當你進行數據庫寫操作時,記住最重要的一件事:在任何時候,只在一個事務下執行一個連接。這回答了第一個問題。 
關於第二個問題,一般來說,一個事務的生命周期和語句差不多,你也可以手動結束它。默認情況下,事務自動提交,當然你也可以通過BEGIN..COMMIT手動提交。接下來的問題 
是事務如何與鎖關聯。

 

鎖的狀態

大多數情況下,鎖的生命周期在事務的生命周期之中。它們不一定同時開始,但總時同時結束。當你結束一個事務時,也會釋放它相關的鎖。或者說,鎖直到事務結束或系統崩潰時才會釋放。如果系統在事務沒有結束的情況下崩潰,那么下一個訪問數據庫的連接會處理這種情況。

讀事務

我們先來看看SELECT語句執行時鎖的狀態變化過程,非常簡單:一個連接執行SELECT語句,觸發一個事務,從UNLOCKED到SHARED,當事務COMMIT時,又回到UNLOCKED,就這么簡單。 
那么,當你運行兩個語句時會發生什么呢?這時如何加鎖呢?這依賴於你是否運行在自動提交狀態。考慮下面的例子(為了簡單,這里用了偽碼): 
db = open('foods.db') 
db.exec('BEGIN') 
db.exec('SELECT * FROM episodes') 
db.exec('SELECT * FROM episodes') 
db.exec('COMMIT') 
db.close() 
由於顯式地使用了BEGIN和COMMIT,兩個SELECT命令在一個事務下執行。第一個exec()執行時,連接處於SHARED,然后第二個exec()執行。當事務提交時,連接又從 
SHARED回到UNLOCKED狀態,狀態變化如下:

UNLOCKED→PENDING→SHARED→UNLOCKED 
如果沒有BEGIN和COMMIT兩行,兩個SELECT都運行於自動提交狀態,狀態變化如下: 
UNLOCKED→PENDING→SHARED→UNLOCKED→PENDING→SHARED→UNLOCKED 
僅僅是讀數據,但在自動提交模式下,卻會經歷兩個加解鎖的循環,太麻煩。而且,一個寫進程可能插到兩個SELECT中間對數據庫進行修改,這樣,你就不能保證第二次能夠讀到 
同樣的數據了,而使用BEGIN..COMMIT就可以有此保證。

 

寫事務

下面我們來考慮寫數據庫,比如UPDATE。和讀事務一樣,它也會經歷UNLOCKED→PENDING→SHARED的變化過程,但接下來就會看到PENDING鎖是如何起到關口作用的了。

保留(RESERVED)狀態

當一個連接(connection)要向數據庫寫數據時,從SHARED狀態變為RESERVED狀態。如果它得到RESERVED鎖,也就意味着它已經准備好進行寫操作了。即使它沒有把修改寫入數據庫,也可以把修改保存到位於pager的緩沖區中(page cache)了。 
當一個連接進入RESERVED狀態,pager就開始初始化回卷日志(rollback journal)。回卷日志是一個文件,用於回卷和崩潰恢復,見圖5-1。在RESERVED狀態下,pager管理着三種頁: 
(1)已修改的頁:包含被B-tree修改的記錄,位於page cache中。 
(2)未修改的頁:包含沒有被B-tree修改的記錄。 
(3)日志頁:這是修改頁以前的版本,日志頁並不存儲在page cache中,而是在B-tree修改頁之前寫入日志。 
Page cache非常重要,正是因為它的存在,一個處於RESERVED狀態的連接可以真正的開始工作,而不會干擾其它的(讀)連接。所以,SQLite可以高效地處理在同一時刻的多個讀連接和一個寫連接。

未決(UNPENDING)狀態

當一個連接完成修改,需要真正開始提交事務時,執行該過程的pager進入EXCLUSIVE狀態。從RESERVED狀態開始,pager試着獲取PENDING鎖,一旦得到,就獨占它,不允 
許任何其它連接獲得PENDING鎖。既然寫操作持有PENDING鎖,其它任何連接都不能從UNLOCKED狀態進入SHARED狀態,即不會再有新的讀進程,也不會再有新的寫進程。 
只有那些已經處於SHARED狀態的連接可以繼續工作。而處於PENDING狀態的寫進程會一直等到所有這些連接釋放它們的鎖,然后對數據庫加EXCUSIVE鎖,進入EXCLUSIVE 
狀態,獨占數據庫。

排它(EXCLUSIVE)狀態

在EXCLUSIVE狀態下,主要的工作是把修改的頁從page cache寫入數據庫文件,這是真正進行寫操作的地方。在pager將修改頁寫到文件之前,還必須先處理日志。它檢查是否所有的日志都寫入了磁盤,因為它們可能還位於操作系統的緩沖區中。所以pager得告訴OS把所有的文件寫入磁盤,這與synchronous pragma所做的工作相同,如第4章所述。 
日志是數據庫進行恢復的惟一方法,所以日志對於DBMS非常重要。如果日志頁沒有完全寫入磁盤而發生崩潰,數據庫就不能恢復到它原來的狀態,此時數據庫就處於不一致狀態。 
日志寫盤完成后,pager就把所有的修改頁寫入數據庫文件。接下來做什么取決於事務提交的模式,如果是自動提交,那么pager清理日志、page cache,然后由EXCLUSIVE進入 
UNLOCKED。如果是手動提交,那么pager繼續持有EXCLUSIVE鎖和回卷日志,直至遇到COMMIT或者ROLLBACK。 
總之,出於性能方面的考慮,進程占有排它鎖的時間應該盡可能的短,所以DBMS通常都是在真正寫文件時才會占有排它鎖,這樣能大大提高並發性能。

 

1.什么是WAL?

      WAL的全稱是Write Ahead Logging,它是很多數據庫中用於實現原子事務的一種機制,SQLite在3.7.0版本引入了該特性。

      2.WAL如何工作?

      在引入WAL機制之前,SQLite使用rollback journal機制實現原子事務。

      rollback journal機制的原理是:在修改數據庫文件中的數據之前,先將修改所在分頁中的數據備份在另外一個地方,然后才將修改寫入到數據庫文件中;如果事務失敗,則將備份數據拷貝回來,撤銷修改;如果事務成功,則刪除備份數據,提交修改。

      WAL機制的原理是:修改並不直接寫入到數據庫文件中,而是寫入到另外一個稱為WAL的文件中;如果事務失敗,WAL中的記錄會被忽略,撤銷修改;如果事務成功,它將在隨后的某個時間被寫回到數據庫文件中,提交修改。

      同步WAL文件和數據庫文件的行為被稱為checkpoint(檢查點),它由SQLite自動執行,默認是在WAL文件積累到1000頁修改的時候;當然,在適當的時候,也可以手動執行checkpoint,SQLite提供了相關的接口。執行checkpoint之后,WAL文件會被清空。

      在讀的時候,SQLite將在WAL文件中搜索,找到最后一個寫入點,記住它,並忽略在此之后的寫入點(這保證了讀寫和讀讀可以並行執行);隨后,它確定所要讀的數據所在頁是否在WAL文件中,如果在,則讀WAL文件中的數據,如果不在,則直接讀數據庫文件中的數據。

      在寫的時候,SQLite將之寫入到WAL文件中即可,但是必須保證獨占寫入,因此寫寫之間不能並行執行。

      WAL在實現的過程中,使用了共享內存技術,因此,所有的讀寫進程必須在同一個機器上,否則,無法保證數據一致性。

      3.WAL的優點與缺點

      優點:

      1.讀和寫可以完全地並發執行,不會互相阻塞(但是寫之間仍然不能並發)。

      2.WAL在大多數情況下,擁有更好的性能(因為無需每次寫入時都要寫兩個文件)。

      3.磁盤I/O行為更容易被預測。

      缺點:

      1.訪問數據庫的所有程序必須在同一主機上,且支持共享內存技術。

      2.每個數據庫現在對應3個文件:<yourdb>.db,<yourdb>-wal,<yourdb>-shm。

      3.當寫入數據達到GB級的時候,數據庫性能將下降。

      4.3.7.0之前的SQLite無法識別啟用了WAL機制的數據庫文件。

      4.WAL引入的兼容性問題

      在啟用了WAL之后,數據庫文件格式的版本號由1升級到了2,因此,3.7.0之前的SQLite無法識別啟用了WAL機制的數據庫文件。

      禁用WAL會使數據庫文件格式的版本號恢復到1,從而可以被SQLite 3.7.0之前的版本識別。

      5.WAL引入的性能問題

      在一般情況下,WAL會提高SQLite的事務性能;但是在某些極端情況下,卻會導致SQLite事務性能的下降。

      1.在事務執行時間較長或者要修改的數據量達到GB級的時候,WAL文件會被占用,它會暫時阻止checkpoint的執行(checkpoint會清空WAL文件),這將導致WAL文件變得很大,增加尋址時間,最終導致讀寫性能的下降。

      2.當checkpoint執行的時候,會降低當時的讀寫性能,因此,WAL可能會導致周期性的性能下降。

      6.與WAL相關的PRAGMA和接口

      PRAGMA journal_mode

      PRAGMA wal_checkpoint

      PRAGMA wal_autocheckpoint

      sqlite3_wal_checkpoint

      sqlite3_wal_autocheckpoint

      sqlite3_wal_hook


免責聲明!

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



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