iOS sqlite3數(shù)據(jù)庫解析

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

多進程可以同時打開同一個數(shù)據(jù)庫晒衩,也可以同時 SELECT 喜德。但只有一個進程可以立即改數(shù)據(jù)庫山橄。

SQLite使用讀/寫鎖定來控制數(shù)據(jù)庫訪問。(Win95/98/ME 操作系統(tǒng)缺乏讀/寫鎖定支持舍悯,在低于 2.7.0 的版本中航棱,這意味著在 windows 下在同一時間內(nèi)只能有一個進程讀數(shù)據(jù)庫。在版本 2.7.0 中 這個問題通過在 windows 接口代碼中執(zhí)行一個用戶間隔幾率讀寫鎖定策略解決了萌衬。) 但如果數(shù)據(jù)庫文件在一個 NFS 文件系統(tǒng)中饮醇,控制并發(fā)讀書的鎖定機制可以會出錯。因為 NFS 的fcntl() 文件鎖定有時會出問題秕豫。如果有多進程可能并發(fā)讀數(shù)據(jù)庫則因當避免把數(shù)據(jù)庫文件放在 NFS 文件系統(tǒng)中朴艰。 根據(jù)微軟的文檔,如果不運行 Share.exe 后臺程序則 FAT 文件系統(tǒng)中的鎖定可能不工作混移。對 Windows 非常有經(jīng)驗的人告訴我網(wǎng)絡文件的鎖定有許多問題并且不可靠祠墅。如果是這樣,在2個或以上 Windows 系統(tǒng)中共享一個 SQLite 數(shù)據(jù)庫文件會導致不可預知的問題歌径。

我們知道沒有其他的嵌入式 SQL數(shù)據(jù)庫引擎比SQLite支持更多的并發(fā)性毁嗦。 SQLite允許多進程 同時打開和讀取數(shù)據(jù)庫。任何一個進程需要寫入時回铛,整個數(shù)據(jù)庫將在這一過程中被鎖定狗准。但這一般僅耗時 幾毫秒芯急。其他進程只需等待然后繼續(xù)其他事務。其他嵌入式SQL數(shù)據(jù)庫引擎往往只允許單進程訪問數(shù)據(jù)庫驶俊。

但是,client/server型的數(shù)據(jù)庫引擎 (如 PostgreSQL, MySQL, 以及 Oracle) 通常支持更高的并發(fā)度免姿, 并支持多進程同時寫入同一個數(shù)據(jù)庫饼酿。由于總有一個控制良好的服務器協(xié)調(diào)數(shù)據(jù)庫的訪問,這才保證了以上 特性的實現(xiàn)胚膊。如果你的應用需要很高的并發(fā)度故俐,你應該考慮使用client/server數(shù)據(jù)庫。事實上紊婉,經(jīng)驗告訴 我們大多數(shù)應用所需要的并發(fā)度比他們的設計者們想象的要少得多药版。

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

如果兩個或更多進程同時打開同一個數(shù)據(jù)庫肢础,其中一個進程創(chuàng)建了新的表或索引还栓,則其它進程可能不能立即看見新的表。其它進程可能需要關(guān)閉并重新連結(jié)數(shù)據(jù)庫传轰。

這幾天研究了一下SQLite這個嵌入式數(shù)據(jù)庫在多線程環(huán)境下的應用剩盒,感覺里面的學問還挺多,于是就在此分享一下慨蛙。

先說下初衷吧辽聊,實際上我經(jīng)常看到有人抱怨SQLite不支持多線程期贫。而在iOS開發(fā)時跟匆,為了不阻塞主線程,數(shù)據(jù)庫訪問必須移到子線程中唯灵。為了解決這個矛盾贾铝,很有必要對此一探究竟。

關(guān)于這個問題埠帕,最權(quán)威的解答當然是SQLite官網(wǎng)上的“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

當然,你也可以自己編譯最新版本呐籽。

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

  1. 單線程:禁用所有的mutex鎖庶橱,并發(fā)使用時會出錯贮勃。當SQLite編譯時加了SQLITE_THREADSAFE=0參數(shù),或者在初始化SQLite前調(diào)用sqlite3_config(SQLITE_CONFIG_SINGLETHREAD)時啟用苏章。
  2. 多線程:只要一個數(shù)據(jù)庫連接不被多個線程同時使用就是安全的寂嘉。源碼中是啟用bCoreMutex,禁用bFullMutex枫绅。實際上就是禁用數(shù)據(jù)庫連接和prepared statement(準備好的語句)上的鎖泉孩,因此不能在多個線程中并發(fā)使用同一個數(shù)據(jù)庫連接或prepared statement。當SQLite編譯時加了SQLITE_THREADSAFE=2參數(shù)時默認啟用并淋。若SQLITE_THREADSAFE不為0寓搬,可以在初始化SQLite前,調(diào)用sqlite3_config(SQLITE_CONFIG_MULTITHREAD)啟用县耽;或者在創(chuàng)建數(shù)據(jù)庫連接時句喷,設置SQLITE_OPEN_NOMUTEX flag。
  3. 串行:啟用所有的鎖兔毙,包括bCoreMutex和bFullMutex脏嚷。因為數(shù)據(jù)庫連接和prepared statement都已加鎖,所以多線程使用這些對象時沒法并發(fā)瞒御,也就變成串行了父叙。當SQLite編譯時加了SQLITE_THREADSAFE=1參數(shù)時默認啟用。若SQLITE_THREADSAFE不為0肴裙,可以在初始化SQLite前趾唱,調(diào)用sqlite3_config(SQLITE_CONFIG_SERIALIZED)啟用;或者在創(chuàng)建數(shù)據(jù)庫連接時蜻懦,設置SQLITE_OPEN_FULLMUTEX flag甜癞。

而這里所說的初始化是指調(diào)用sqlite3_initialize()函數(shù),這個函數(shù)在調(diào)用sqlite3_open()時會自動調(diào)用宛乃,且只有第一次調(diào)用是有效的悠咱。
另一個要說明的是prepared statement,它是由數(shù)據(jù)庫連接(的pager)來管理的征炼,使用它也可看成使用這個數(shù)據(jù)庫連接析既。因此在多線程模式下,并發(fā)對同一個數(shù)據(jù)庫連接調(diào)用sqlite3_prepare_v2()來創(chuàng)建prepared statement谆奥,或者對同一個數(shù)據(jù)庫連接的任何prepared statement并發(fā)調(diào)用sqlite3_bind_*()和sqlite3_step()等函數(shù)都會出錯(在iOS上眼坏,該線程會出現(xiàn)EXC_BAD_ACCESS而中止)。這種錯誤無關(guān)讀寫酸些,就是只讀也會出錯宰译。文檔中給出的安全使用規(guī)則是:沒有事務正在等待執(zhí)行檐蚜,所有prepared statement都被finalized
順帶一提沿侈,調(diào)用sqlite3_threadsafe()可以獲得編譯期的SQLITE_THREADSAFE參數(shù)闯第。標準發(fā)行版是1,也就是串行模式缀拭;而iOS上是2乡括,也就是多線程模式;Python的sqlite3模塊也默認使用串行模式智厌,可以用sqlite3.threadsafety來配置。但是默認情況下盲赊,一個線程只能使用當前線程打開的數(shù)據(jù)庫連接铣鹏,除非在連接時設置了check_same_thread=False參數(shù)。

現(xiàn)在3種模式都有所了解了哀蘑,清楚SQLite并不是對多線程無能為力后诚卸,接下來就了解下事務吧。
數(shù)據(jù)庫只有在事務中才能被更改绘迁。所有更改數(shù)據(jù)庫的命令(除SELECT以外的所有SQL命令)都會自動開啟一個新事務合溺,并且當最后一個查詢完成時自動提交。
而BEGIN命令可以手動開始事務缀台,并關(guān)閉自動提交棠赛。當下一條COMMIT命令執(zhí)行時,自動提交再次打開膛腐,事務中所做的更改也被寫入數(shù)據(jù)庫睛约。當COMMIT失敗時,自動提交仍然關(guān)閉哲身,以便讓用戶嘗試再次提交辩涝。若執(zhí)行的是ROLLBACK命令,則也打開自動提交勘天,但不保存事務中的更改怔揩。關(guān)閉數(shù)據(jù)庫或遇到錯誤時,也會自動回滾事務脯丝。
經(jīng)常有人抱怨SQLite的插入太慢商膊,實際上它可以做到每秒插入幾萬次,但是每秒只能提交幾十次事務宠进。因此在插入大批數(shù)據(jù)時翘狱,可以通過禁用自動提交來提速。

事務在改寫數(shù)據(jù)庫文件時砰苍,會先生成一個rollback journal(回滾日志)潦匈,記錄初始狀態(tài)(其實就是備份)阱高,所有改動都是在數(shù)據(jù)庫文件上進行的。當事務需要回滾時茬缩,可以將備份文件的內(nèi)容還原到數(shù)據(jù)庫文件赤惊;提交成功時,默認的delete模式下會直接刪除這個日志凰锡。這個日志也可以幫助解決事務執(zhí)行過程中斷電未舟,導致數(shù)據(jù)庫文件損壞的問題。但如果操作系統(tǒng)或文件系統(tǒng)有bug掂为,或是磁盤損壞裕膀,則仍有可能無法恢復。
而從3.7.0版本(對應iOS 4.3)開始勇哗,SQLite還提供了Write-Ahead Logging模式昼扛。與delete模式相比,WAL模式在大部分情況下更快欲诺,并發(fā)性更好抄谐,讀和寫之間互不阻塞;而其缺點對于iPhone這種嵌入式設備來說可以忽略扰法,只需注意不要以只讀方式打開WAL模式的數(shù)據(jù)庫即可蛹含。
使用WAL模式時,改寫操是附加(append)到WAL文件塞颁,而不改動數(shù)據(jù)庫文件浦箱,因此數(shù)據(jù)庫文件可以被同時讀取。當執(zhí)行checkpoint操作時祠锣,WAL文件的內(nèi)容會被寫回數(shù)據(jù)庫文件憎茂。當WAL文件達到SQLITE_DEFAULT_WAL_AUTOCHECKPOINT(默認值是1000)頁(默認大小是1KB)時,會自動使用當前COMMIT的線程來執(zhí)行checkpoint操作锤岸。也可以關(guān)閉自動checkpoint竖幔,改為手動定期checkpoint。
為了避免讀取的數(shù)據(jù)不一致是偷,查詢時也需要讀取WAL文件拳氢,并記錄一個結(jié)尾標記(end mark)。這樣的代價就是讀取會變得稍慢蛋铆,但是寫入會變快很多馋评。要提高查詢性能的話,可以減小WAL文件的大小刺啦,但寫入性能也會降低留特。
需要注意的是,低版本的SQLite不能讀取高版本的SQLite生成的WAL文件,但是數(shù)據(jù)庫文件是通用的蜕青。這種情況在用戶進行iOS降級時可能會出現(xiàn)苟蹈,可以把模式改成delete,再改回WAL來修復右核。
要對一個數(shù)據(jù)庫連接啟用WAL模式慧脱,需要執(zhí)行“PRAGMA journal_mode=WAL;”這條命令,它的默認值是“journal_mode=DELETE”贺喝。執(zhí)行后會返回新的journal_mode字符串值菱鸥,即成功時為"wal",失敗時為之前的模式(例如"delete")躏鱼。一旦啟用WAL模式后氮采,數(shù)據(jù)庫會保持這個模式,這樣下次打開數(shù)據(jù)庫時仍然是WAL模式染苛。
要停止自動checkpoint鹊漠,可以使用wal_autocheckpoint指令或sqlite3_wal_checkpoint()函數(shù)。手動執(zhí)行checkpoint可以使用wal_checkpoint指令或sqlite3_wal_checkpoint()函數(shù)殖侵。

還有一個很重要的知識點需要強調(diào):事務是和數(shù)據(jù)庫連接相關(guān)的,每個數(shù)據(jù)庫連接(使用pager來)維護自己的事務镰烧,且同時只能有一個事務(但是可以用SAVEPOINT來實現(xiàn)內(nèi)嵌事務)拢军。
也就是說,事務與線程無關(guān)怔鳖,一個線程里可以同時用多個數(shù)據(jù)庫連接來完成多個事務茉唉,而多個線程也可以同時(非并發(fā))使用一個數(shù)據(jù)庫連接來共同完成一個事務。
下面用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()

在這個例子中结执,雖然是在子線程中執(zhí)行rollback度陆,但由于和主線程用的是同一個數(shù)據(jù)庫連接,所以主線程所做的更改也被回滾了献幔。
而如果是用不同的數(shù)據(jù)庫連接懂傀,每個連接都不能讀取其他連接中未提交的數(shù)據(jù),除非使用read-uncommitted模式蜡感。

而要實現(xiàn)事務,就不得不用到
一個SQLite數(shù)據(jù)庫文件有5種鎖的狀態(tài):

  • UNLOCKED:表示數(shù)據(jù)庫此時并未被讀寫湿硝。
  • SHARED:表示數(shù)據(jù)庫可以被讀取律胀。SHARED鎖可以同時被多個線程擁有。一旦某個線程持有SHARED鎖情连,就沒有任何線程可以進行寫操作叽粹。
  • RESERVED:表示準備寫入數(shù)據(jù)庫。RESERVED鎖最多只能被一個線程擁有,此后它可以進入PENDING狀態(tài)虫几。
  • PENDING:表示即將寫入數(shù)據(jù)庫锤灿,正在等待其他讀線程釋放SHARED鎖。一旦某個線程持有PENDING鎖持钉,其他線程就不能獲取SHARED鎖衡招。這樣一來,只要等所有讀線程完成每强,釋放SHARED鎖后始腾,它就可以進入EXCLUSIVE狀態(tài)了。
  • EXCLUSIVE:表示它可以寫入數(shù)據(jù)庫了空执。進入這個狀態(tài)后浪箭,其他任何線程都不能訪問數(shù)據(jù)庫文件。因此為了并發(fā)性辨绊,它的持有時間越短越好奶栖。

一個線程只有在擁有低級別的鎖的時候,才能獲取更高一級的鎖门坷。SQLite就是靠這5種類型的鎖宣鄙,巧妙地實現(xiàn)了讀寫線程的互斥。同時也可看出默蚌,寫操作必須進入EXCLUSIVE狀態(tài)冻晤,此時并發(fā)數(shù)被降到1,這也是SQLite被認為并發(fā)插入性能不好的原因绸吸。
另外鼻弧,read-uncommitted和WAL模式會影響這個鎖的機制。在這2種模式下锦茁,讀線程不會被寫線程阻塞攘轩,即使寫線程持有PENDING或EXCLUSIVE鎖。

提到鎖就不得不說到死鎖的問題码俩,而SQLite也可能出現(xiàn)死鎖度帮。
下面舉個例子:

連接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個連接都在等待對方釋放鎖冕屯,于是就死鎖了。當然拂苹,實際情況并沒那么糟糕安聘,任何一方選擇不繼續(xù)等待痰洒,回滾事務就行了。

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

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

現(xiàn)在考慮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狀態(tài)長期阻塞其他請求尾膊,最好的方式還是讓所有寫事務都以IMMEDIATE方式開始媳危。
順帶一提,要實現(xiàn)重試的話冈敛,可以使用sqlite3_busy_timeout()或sqlite3_busy_handler()函數(shù)待笑。

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

  1. SQLite使用單線程模式暮蹂,用一個專門的線程訪問數(shù)據(jù)庫。
  2. SQLite使用單線程模式癌压,用一個線程隊列來訪問數(shù)據(jù)庫仰泻,隊列一次只允許一個線程執(zhí)行,隊列里的線程共用一個數(shù)據(jù)庫連接滩届。
  3. SQLite使用多線程模式集侯,每個線程創(chuàng)建自己的數(shù)據(jù)庫連接。
  4. SQLite使用串行模式,所有線程共用全局的數(shù)據(jù)庫連接棠枉。

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

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

第二種方式可以用dispatch_queue_create()來創(chuàng)建一個serial queue贱除,或者用一個maxConcurrentOperationCount為1的NSOperationQueue來實現(xiàn)生闲。
這種方式的缺點就是事務必須在一個block或operation里完成,否則會亂序勘伺;而耗時較長的事務會阻塞隊列跪腹。另外,沒法利用多核CPU的優(yōu)勢飞醉。

先初始化數(shù)據(jù)庫:

#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條測試數(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)建一個串行隊列:

static dispatch_queue_t queue;

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

再設置一個計數(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];
}

這樣就可以開始測試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。
因為block是在棧里生成的缅帘,異步執(zhí)行時已經(jīng)被銷毀轴术,所以需要copy到堆。因為需要一直執(zhí)行钦无,所以我就沒release了逗栽。
此外,光copy的話還是無法正常執(zhí)行失暂,但是把block本身的存儲類型設為__block后就正常了彼宠,原因我也不清楚。

測試結(jié)果為只讀時平均每秒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文件過大而影響性能

測試結(jié)果為只讀時平均每秒166次系宫,只寫時每秒244次索昂,同時讀寫時每秒各97次。并發(fā)性增加了1倍有木有扩借!更夸張的是寫入比讀取還快了椒惨。

在自編譯的3.7.8版中,同時讀寫為每秒各102次潮罪,加上SQLITE_THREADSAFE=0參數(shù)后為每秒各104次康谆,性能稍有提升凄杯。

第三種方式需要打開和關(guān)閉數(shù)據(jù)庫連接,所以會額外消耗一些時間秉宿。此外還要維持各個連接間的互斥戒突,事務也比較容易沖突,但能確保事務正確執(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);

隊列改成可以亂序執(zhí)行的:

queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

然后是訪問數(shù)據(jù)庫:

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)即可忱叭。

測試結(jié)果為只讀時平均每秒164次隔崎,只寫時每秒68次,同時讀寫時分別為每秒14和30次(波動很大)韵丑。此外爵卒,這種方式因為最初啟動的幾個線程持續(xù)訪問數(shù)據(jù)庫,后加入的線程會滯后幾秒才啟動撵彻,且很難打開數(shù)據(jù)庫連接或創(chuàng)建prepare statement钓株。調(diào)試時發(fā)現(xiàn)只會啟用2個線程,但是隨隊列中block數(shù)目的增加陌僵,讀性能增高轴合,寫性能降低。讀寫各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);
    });
}

結(jié)果為只讀時平均每秒169次,只寫時每秒246次偎谁,同時讀寫時每秒分別為90和57次(波動較大)总滩。并發(fā)效率有了顯著提升,但仍不及第二種方式巡雨。

第四種方式相當于讓SQLite來維護隊列闰渔,只不過SQL的執(zhí)行是亂序的,因此無法保證事務性鸯隅。

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

sqlite3_config(SQLITE_CONFIG_SERIALIZED);

再是訪問數(shù)據(jù)庫:

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));
        }
    });
}

測試結(jié)果為只讀時平均每秒164次向挖,只寫時每秒68次蝌以,同時讀寫時每秒分別為57和43次。讀線程比寫線程的速率更高何之,而且新線程的加入不需要等待跟畅。
WAL模式下,只讀時平均每秒176次溶推,只寫時每秒254次徊件,同時讀寫時每秒分別為109和85次奸攻。

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

剛才洗澡時我又想到一點,既然第2種方式不能并行堰燎,第4種方式不能保證事務性掏父,那么能否將各自的優(yōu)點結(jié)合起來呢?
于是一個新的實現(xiàn)方案又浮出水面了:使用2個串行隊列秆剪,分別負責讀和寫赊淑,每個隊列各使用一個數(shù)據(jù)庫連接,線程模式可以采用多線程或串行模式仅讽。
代碼拿方式2稍做修改就行了陶缺,這里就不列出了。測試結(jié)果波動比較大(估計是checkpoint的影響)洁灵,多線程模式下平均約為89和73次饱岸,串行模式下為91和86次。
但在iPad 2這種雙核的機型上徽千,多線程明顯要比單隊列更具優(yōu)勢:方式2的成績是每秒各85次苫费,方式3是94和124次(寫波動較大),方式4是95和72次双抽,而新方案在多線程模式下是104和168次(寫波動很大百框,40~280之間),串行模式下為108和177次(寫波動很大)牍汹。
因此極端的優(yōu)化情況下铐维,可以根據(jù)CPU核心數(shù)來創(chuàng)建隊列數(shù)柬泽,然后把數(shù)據(jù)庫訪問線程隨機分配到某個隊列中。不過考慮到iOS設備這種嵌入式平臺并不需要密集地訪問數(shù)據(jù)庫嫁蛇,而且除數(shù)據(jù)庫線程以外還有其他事要做锨并,如果沒遇到瓶頸的話,簡單的方案2其實也夠用了睬棚。

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

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

 所以建議:

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

 此模式下多個線程必須各自使用自己的數(shù)據(jù)庫連接闸拿,多個連接對SQLite的操作不用擔心空盼,因為SQLite是線程安全的。

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

 所以,采用FMDB queue(其實就是GCD串行隊列)严衬,因為FMDB queue串行隊列澄者,所以所有的數(shù)據(jù)操作包括事務都是串行執(zhí)行的,所有多線程發(fā)起的數(shù)據(jù)庫操作都被異步放到了這個隊列里请琳,不會卡住UI粱挡,也不會有數(shù)據(jù)庫事務并發(fā)問題;

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

按1)優(yōu)化后也有可能性能還是慢的情況询筏,也就是如2)題,這種查詢特別慢竖慧,嚴重影響了事務的執(zhí)行嫌套;所以盡量避免在事務中有大數(shù)據(jù)量條件查詢;

3)最后圾旨,把數(shù)據(jù)庫的WAL模式開啟踱讨,這樣數(shù)據(jù)庫的寫操作很快,而且可以對數(shù)據(jù)并發(fā)寫和讀忍几臁勇蝙;

  1. WAL is significantly faster in most scenarios.

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

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

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

綜上沫勿,總結(jié)如下:

1)數(shù)據(jù)庫多線程模下挨约,有并發(fā)事務或并發(fā)事務多會超成數(shù)據(jù)庫操作擁堵味混,這個與數(shù)據(jù)庫模式無關(guān)(FMDB queue解決)

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

2)開啟WAL模式;

3)區(qū)分對待數(shù)據(jù)庫串行模式和串行數(shù)據(jù)庫操作隊列诫惭;

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

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

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

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

sqlite3中的常見問題

sqlite常見問題

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

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

長一點的答案: 如果你聲明表的一列為 INTEGER PRIMARY KEY,那么赢笨, 每當你在該列上插入一NULL值時未蝌, NULL自動被轉(zhuǎn)換為一個比該列中最大值大1的一個整數(shù),如果表是空的茧妒, 將會是1萧吠。 (如果是最大可能的主鍵 9223372036854775807,那個桐筏,將鍵值將是隨機未使用的數(shù)纸型。) 如,有下列表:

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()绊袋, 它將返回最近插入的整數(shù)值。

注意該整數(shù)會比表中該列上的插入之前的最大值大1铸鹰。 該鍵值在當前的表中是唯一的癌别。但有可能與已從表中刪除的值重疊。 要想建立在整個表的生命周期中唯一的鍵值蹋笼,需要在 INTEGER PRIMARY KEY 上增加AUTOINCREMENT聲明展姐。那么,新的鍵值將會比該表中曾能存在過的最大值大1剖毯。 如果最大可能的整數(shù)值在數(shù)據(jù)表中曾經(jīng)存在過圾笨,INSERT將會失敗, 并返回SQLITE_FULL錯誤代碼逊谋。


(2)SQLite3支持何種數(shù)據(jù)類型擂达?

NULL
INTEGER
REAL
TEXT
BLOB
但實際上,sqlite3也接受如下的數(shù)據(jù)類型:
smallint 16 位元的整數(shù)胶滋。
interger 32 位元的整數(shù)板鬓。
decimal(p,s) p 精確值和 s 大小的十進位整數(shù)悲敷,精確值p是指全部有幾個數(shù)(digits)大小值 ,s是指小數(shù)點後有幾位數(shù)俭令。如果沒有特別指定后德,則系統(tǒng)會設為 p=5; s=0 闸氮。
float 32位元的實數(shù)赚爵。
double 64位元的實數(shù)。
char(n) n 長度的字串析藕,n不能超過 254赫蛇。
varchar(n) 長度不固定且其最大長度為 n 的字串绵患,n不能超過 4000。
graphic(n) 和 char(n) 一樣悟耘,不過其單位是兩個字元 double-bytes藏雏, n不能超過127。 這個形態(tài)是為了支援兩個字元長度的字體作煌,例如中文字掘殴。
vargraphic(n) 可變長度且其最大長度為 n 的雙字元字串,n不能超過 2000粟誓。
date 包含了 年份奏寨、月份、日期鹰服。
time 包含了 小時病瞳、分鐘、秒悲酷。
timestamp 包含了 年套菜、月、日设易、時逗柴、分、秒顿肺、千分之一秒戏溺。


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

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

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


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

主鍵必須是數(shù)值類型,將主鍵改為TEXT型將不起作用愿汰。

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


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

多個進程可同時打開同一個數(shù)據(jù)庫。多個進程可以同時進行SELECT 操作跌宛,但在任一時刻酗宋,只能有一個進程對數(shù)據(jù)庫進行更改。

SQLite使用讀疆拘、寫鎖控制對數(shù)據(jù)庫的訪問蜕猫。(在Win95/98/ME等不支持讀、 寫鎖的系統(tǒng)下哎迄,使用一個概率性的模擬來代替丹锹。)但使用時要注意: 如果數(shù)據(jù)庫文件存放于一個NFS文件系統(tǒng)上,這種鎖機制可能不能正常工作芬失。 這是因為 fcntl() 文件鎖在很多NFS上沒有正確的實現(xiàn)楣黍。 在可能有多個進程同時訪問數(shù)據(jù)庫的時候,應該避免將數(shù)據(jù)庫文件放到NFS上棱烂。 在Windows上租漂,Microsoft的文檔中說:如果使用 FAT 文件系統(tǒng)而沒有運行 share.exe 守護進程,那么鎖可能是不能正常使用的。那些在Windows上有很多經(jīng)驗的人告訴我: 對于網(wǎng)絡文件哩治,文件鎖的實現(xiàn)有好多Bug秃踩,是靠不住的。如果他們說的是對的业筏, 那么在兩臺或多臺Windows機器間共享數(shù)據(jù)庫可能會引起不期望的問題憔杨。

我們意識到,沒有其它嵌入式的 SQL 數(shù)據(jù)庫引擎能象 SQLite 這樣處理如此多的并發(fā)蒜胖。SQLite允許多個進程同時打開一個數(shù)據(jù)庫消别, 同時讀一個數(shù)據(jù)庫。當有任何進程想要寫時台谢,它必須在更新過程中鎖住數(shù)據(jù)庫文件寻狂。 但那通常只是幾毫秒的時間。其它進程只需等待寫進程干完活結(jié)束朋沮。 典型地蛇券,其它嵌入式的SQL數(shù)據(jù)庫引擎同時只允許一個進程連接到數(shù)據(jù)庫。

但是樊拓,Client/Server數(shù)據(jù)庫引擎(如 PostgreSQL, MySQL, 或 Oracle) 通常支持更高級別的并發(fā)纠亚,并且允許多個進程同時寫同一個數(shù)據(jù)庫。 這種機制在Client/Server結(jié)構(gòu)的數(shù)據(jù)庫上是可能的筋夏, 因為總是有一個單一的服務器進程很好地控制蒂胞、協(xié)調(diào)對數(shù)據(jù)庫的訪問。 如果你的應用程序需要很多的并發(fā)叁丧,那么你應該考慮使用一個Client/Server 結(jié)構(gòu)的數(shù)據(jù)庫啤誊。但經(jīng)驗表明,很多應用程序需要的并發(fā)拥娄,往往比其設計者所想象的少得多蚊锹。

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

(6)SQLite線程安全嗎?

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

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

在 3.3.1 版本之前,一個 sqlite3 結(jié)構(gòu)只能被用于調(diào)用 sqlite3_open 創(chuàng)建的同一線程躺屁。你不能在一個線程中打開數(shù)據(jù)庫肯夏, 然后將數(shù)據(jù)庫句柄傳遞給另外一個進程使用。 這主要是由于在好多通用的線程實現(xiàn)(如RedHat9)中的限制引起的(是Bug嗎犀暑?)驯击。 特別的,在有問題的系統(tǒng)上母怜,一個進程創(chuàng)建的 fcntl() 鎖無法被其它線程清除或修改余耽。 所以缚柏,由于 SQLite 大量使用 fcntl() 鎖做并發(fā)控制苹熏, 如果你在不同的線程間移動數(shù)據(jù)庫連接,就可能會出現(xiàn)嚴重的問題币喧。

在3.3.1版本上轨域,關(guān)于在線程間移動數(shù)據(jù)庫連接的限制變得寬松了。 在它及以后的版本中杀餐,只要連接沒有持有 fcntl() 鎖干发,在線程間移動句柄是安全的。 如果沒有未決的事務史翘,并且所有的語句都已執(zhí)行完畢枉长, 你就可以安全的假定不再持有任何鎖。

在UNIX中琼讽,在執(zhí)行 fork() 系統(tǒng)調(diào)用時不應攜帶已打開的數(shù)據(jù)庫進入子進程必峰。 那樣做將會有問題。


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

如果你運行 sqlite3 命令行來訪問你的數(shù)據(jù)庫吼蚁,可以鍵入 “.tables”來獲得所有表的列表∥是罚或者肝匆,你可以輸入 “.schema” 來看整個數(shù)據(jù)庫模式,包括所有的表的索引顺献。 輸入這些命令旗国,后面跟一個LIKE模式匹配可以限制顯示的表。

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

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

對于表來說,type 字段永遠是 'table'借浊,name 字段永遠是表的名字塘淑。所以,要獲得數(shù)據(jù)庫中所有表的列表蚂斤, 使用下列SELECT語句:

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

對于索引存捺,type 等于 'index', name 則是索引的名字,tbl_name 是該索引所屬的表的名字曙蒸。 不管是表還是索引捌治,sql 字段是原先用 CREATE TABLE 或 CREATE INDEX 語句創(chuàng)建它們時的命令文本。對于自動創(chuàng)建的索引(用來實現(xiàn) PRIMARY KEY 或 UNIQUE 約束)纽窟,sql字段為NULL肖油。

SQLITE_MASTER 表是只讀的。不能對它使用 UPDATE臂港、INSERT 或 DELETE森枪。 它會被 CREATE TABLE、CREATE INDEX审孽、DROP TABLE 和 DROP INDEX 命令自動更新县袱。

臨時表不會出現(xiàn)在 SQLITE_MASTER 表中。臨時表及其索引和觸發(fā)器存放在另外一個叫 SQLITE_TEMP_MASTER 的表中佑力。SQLITE_TEMP_MASTER 跟 SQLITE_MASTER 差不多式散, 但它只是對于創(chuàng)建那些臨時表的應用可見。如果要獲得所有表的列表打颤, 不管是永久的還是臨時的暴拄,可以使用類似下面的命令:

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


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

在 Windows 和 Unix 下编饺,版本 2.7.4的 SQLite 可以達到 2的41次方字節(jié) (2T 字節(jié))乖篷。老版本的為 2的31 次方字節(jié)(2G 字節(jié))。
SQLite 版本 2.8 限制一個記錄的容量為 1M反肋。SQLite 版本 3.0 則對單個記錄容量沒有限制那伐。
表名、索引表名石蔗、視圖名罕邀、觸發(fā)器名和字段名沒有長度限制。但 SQL 函數(shù)的名稱 (由 sqlite3_create_function() API 函數(shù)創(chuàng)建) 不得超過 255 個字符养距。

對有關(guān)SQLite限制的詳細討論诉探,見 limits.html 。


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

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


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

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


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

SQLite 有有限地 ALTER TABLE 支持。你可以使用它來在表的末尾增加一列陆爽,可更改表的名稱什往。 如果需要對表結(jié)構(gòu)做更復雜的改變,則必須重新建表慌闭。 重建時可以先將已存在的數(shù)據(jù)放到一個臨時表中别威,刪除原表, 創(chuàng)建新表贡必,然后將數(shù)據(jù)從臨時表中復制回來兔港。

如庸毫,假設有一個 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) 我在數(shù)據(jù)庫中刪除了很多數(shù)據(jù)利花,但數(shù)據(jù)庫文件沒有變小,是Bug嗎载佳?

不是炒事。當你從SQLite數(shù)據(jù)庫中刪除數(shù)據(jù)時, 未用的磁盤空間將會加入一個內(nèi)部的“自由列表”中蔫慧。 當你下次插入數(shù)據(jù)時挠乳,這部分空間可以重用。磁盤空間不會丟失姑躲, 但也不會返還給操作系統(tǒng)睡扬。

如果刪除了大量數(shù)據(jù),而又想縮小數(shù)據(jù)庫文件占用的空間黍析,執(zhí)行 VACUUM 命令卖怜。 VACUUM 將會從頭重新組織數(shù)據(jù)庫。這將會使用數(shù)據(jù)庫有一個空的“自由鏈表”阐枣, 數(shù)據(jù)庫文件也會最小马靠。但要注意的是奄抽,VACUUM 的執(zhí)行會需要一些時間 (在SQLite開發(fā)時,在Linux上甩鳄,大約每M字節(jié)需要半秒種)逞度,并且, 執(zhí)行過程中需要原數(shù)據(jù)庫文件至多兩倍的臨時磁盤空間妙啃。

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


(13) 我可以在商業(yè)產(chǎn)品中使用SQLite而不需支付許可費用嗎茁瘦?

是的。SQLite 在 public domain储笑。 對代碼的任何部分沒有任何所有權(quán)聲明甜熔。你可以使用它做任何事。


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

SQL 標準規(guī)定腔稀,在字符串中,單引號需要使用逃逸字符羽历,即在一行中使用兩個單引號焊虏。在這方面 SQL 用起來類似 Pascal 語言。 SQLite 尊循標準秕磷。如:

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


(15) SQLITE_SCHEMA error是什么錯誤诵闭?為什么會出現(xiàn)該錯誤?

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

準備好的語句失效的最通常原因是:在語句準備好后晤郑, 數(shù)據(jù)庫的模式又被修改了敌呈。另外的原因會發(fā)生在:

數(shù)據(jù)庫離線:DETACHed.
數(shù)據(jù)庫被 VACUUMed
一個用戶存儲過程定義被刪除或改變。
一個 collation 序列定義被刪除或改變造寝。
認證函數(shù)被改變磕洪。
在所有情況下,解決方法是重新編譯并執(zhí)行該SQL語句匹舞。 因為一個已準備好的語句可以由于其它進程改變數(shù)據(jù)庫模式而失效褐鸥, 所有使用 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 使用二進制算術(shù)浑侥,在二進制中, 無法用有限的二進制位數(shù)表示 9.95 晰绎。使用 64-bit IEEE 浮點 (SQLite就是使用這個)最接近 9.95 的二進制表示是 9.949999999999999289457264239899814128875732421875寓落。 所在,當你輸入 9.95 時荞下,SQLite實際上以為是上面的數(shù)字伶选, 在四舍五入時會舍去。

這種問題在使用二進制浮點數(shù)的任何時候都會出現(xiàn)尖昏。 通常的規(guī)則是記住很多有限的十進制小數(shù)都沒有一個對應的二進制表示仰税。 所以,它們只能使用最接近的二進制數(shù)抽诉。它們通常非常接近陨簇, 但也會有些微小的不同,有些時候也會導致你所期望的不同的結(jié)果迹淌。

SQLITE的鎖

在SQLite中河绽,鎖和事務是緊密聯(lián)系的。為了有效地使用事務唉窃,需要了解一些關(guān)于如何加鎖的知識耙饰。
SQLite采用粗放型的鎖。當一個連接要寫數(shù)據(jù)庫纹份,所有其它的連接被鎖住苟跪,直到寫連接結(jié)束了它的事務。SQLite有一個加鎖表矮嫉,來幫助不同的寫數(shù)據(jù)庫都能夠在最后一刻再加鎖削咆,
以保證最大的并發(fā)性。
SQLite使用鎖逐步上升機制蠢笋,為了寫數(shù)據(jù)庫,連接需要逐級地獲得排它鎖鳞陨。SQLite有5個不同的鎖狀態(tài):未加鎖(UNLOCKED)昨寞、共享(SHARED)、保留(RESERVED)厦滤、未決(PENDING)和排它(EXCLUSIVE)援岩。每個數(shù)據(jù)庫連接在同一時刻只能處于其中一個狀態(tài)。每種狀態(tài)(未加
鎖狀態(tài)除外)都有一種鎖與之對應掏导。
最初的狀態(tài)是未加鎖狀態(tài)享怀,在此狀態(tài)下,連接還沒有存取數(shù)據(jù)庫趟咆。當連接到了一個數(shù)據(jù)庫添瓷,甚至已經(jīng)用BEGIN開始了一個事務時梅屉,連接都還處于未加鎖狀態(tài)。未加鎖狀態(tài)的下一個狀態(tài)是共享狀態(tài)鳞贷。為了能夠從數(shù)據(jù)庫中讀(不寫)數(shù)據(jù)坯汤,連接必須首先進入共享狀態(tài),也就是說首先要獲得一個共享鎖搀愧。多個連接可以同時獲得并保持共享鎖惰聂,也就是說多個連接可以同時從同一個數(shù)據(jù)庫中讀數(shù)據(jù)。但哪怕只有一個共享鎖還沒有釋放咱筛,也不允許任何連接寫數(shù)據(jù)庫搓幌。

如果一個連接想要寫數(shù)據(jù)庫,它必須首先獲得一個保留鎖迅箩。一個數(shù)據(jù)庫上同時只能有一個保 留鎖鼻种。保留鎖可以與共享鎖共存,保留鎖是寫數(shù)據(jù)庫的第1階段沙热。保留鎖即不阻止其它擁有 共享鎖的連接繼續(xù)讀數(shù)據(jù)庫叉钥,也不阻止其它連接獲得新的共享鎖。 一旦一個連接獲得了保留鎖篙贸,它就可以開始處理數(shù)據(jù)庫修改操作了投队,盡管這些修改只能在
緩沖區(qū)中進行,而不是實際地寫到磁盤爵川。對讀出內(nèi)容所做的修改保存在內(nèi)存緩沖區(qū)中敷鸦。 當連接想要提交修改(或事務)時,需要將保留鎖提升為排它鎖寝贡。為了得到排它鎖扒披,還必須首
先將保留鎖提升為未決鎖。獲得未決鎖之后圃泡,其它連接就不能再獲得新的共享鎖了碟案,但已經(jīng)擁有共享鎖的連接仍然可以繼續(xù)正常讀數(shù)據(jù)庫。此時颇蜡,擁有未決鎖的連接等待其它擁有共享鎖的連接完成工作并釋放其共享鎖价说。
一旦所有其它共享鎖都被釋放,擁有未決鎖的連接就可以將其鎖提升至排它鎖风秤,此時就可以自由地對數(shù)據(jù)庫進行修改了鳖目。所有以前對緩沖區(qū)所做的修改都會被寫到數(shù)據(jù)庫文件。

事務的種類

SQLite有三種不同的事務缤弦,使用不同的鎖狀態(tài)领迈。事務可以開始于:DEFERRED、MMEDIATE或EXCLUSIVE。事務類型在BEGIN命令中指定:
BEGIN [ DEFERRED | IMMEDIATE | EXCLUSIVE ] TRANSACTION;
一個DEFERRED事務不獲取任何鎖(直到它需要鎖的時候)狸捅,BEGIN語句本身也不會做什么事情——它開始于UNLOCK狀態(tài)衷蜓。默認情況下就是這樣的,如果僅僅用BEGIN開始一個事
務薪贫,那么事務就是DEFERRED的恍箭,同時它不會獲取任何鎖;當對數(shù)據(jù)庫進行第一次讀操作時瞧省,它會獲取SHARED鎖扯夭;同樣,當進行第一次寫操作時鞍匾,它會獲取RESERVED鎖交洗。由BEGIN開始的IMMEDIATE 事務會嘗試獲取RESERVED鎖。如果成功橡淑,BEGIN IMMEDIATE保證沒有別的連接可以寫數(shù)據(jù)庫构拳。但是,別的連接可以對數(shù)據(jù)庫進行讀操作梁棠;但是置森,RESERVED鎖會阻止其它連接的BEGIN IMMEDIATE或者BEGIN EXCLUSIVE命令,當其它連接執(zhí)行上述命令時符糊,會返回SQLITE_BUSY錯誤凫海。這時你就可以對數(shù)據(jù)庫進行
修改操作了突那,但是你還不能提交剔交,當你COMMIT時计寇,會返回SQLITE_BUSY錯誤聪姿,這意味著還有其它的讀事務沒有完成,得等它們執(zhí)行完后才能提交事務查剖。

EXCLUSIVE事務會試著獲取對數(shù)據(jù)庫的EXCLUSIVE鎖洁桌。這與IMMEDIATE類似荤牍,但是一旦成功尸折,EXCLUSIVE事務保證沒有其它的連接啰脚,所以就可對數(shù)據(jù)庫進行讀寫操作了。
上節(jié)那個例子的問題在于兩個連接最終都想寫數(shù)據(jù)庫翁授,但是它們都沒有放棄各自原來的鎖最終拣播,SHARED鎖導致了問題的出現(xiàn)。如果兩個連接都以BEGIN IMMEDIATE開始事務收擦,
那么死鎖就不會發(fā)生。在這種情況下谍倦,在同一時刻只能有一個連接進入BEGIN IMMEDIATE塞赂,其它的連接就得等待。BEGIN IMMEDIATE和BEGIN EXCLUSIVE通常被寫
事務使用昼蛀。就像同步機制一樣宴猾,它防止了死鎖的產(chǎn)生圆存。基本的準則是:如果你正在使用的數(shù)據(jù)庫沒有其它的連接仇哆,用BEGIN就足夠了沦辙。但是,如果你使用的數(shù)據(jù)庫有其它的連接也會對數(shù)據(jù)庫進行寫操作讹剔,就得使用BEGIN IMMEDIATE或BEGIN EXCLUSIVE開始你的事務油讯。

事務的生命周期

有一些關(guān)于代碼和事務的問題需要關(guān)注。首先需要知道哪個對象運行在哪個事務之下延欠。另一個問題是持續(xù)時間——一個事務何時開始陌兑,何時結(jié)束,從哪一點開始影響其它連接由捎?第一
個問題與API直接關(guān)聯(lián)兔综,第二個與SQL的一般概念及SQLite的特殊實現(xiàn)關(guān)聯(lián)。
一個連接(connection)可以包含多個語句(statement)狞玛,而且每個連接有一個與數(shù)據(jù)庫關(guān)聯(lián)的B-tree和一個pager软驰。Pager在連接中起著很重要的作用,因為它管理事務心肪、鎖锭亏、內(nèi)存緩沖以及負責崩潰恢復(crash recovery)。當你進行數(shù)據(jù)庫寫操作時蒙畴,記住最重要的一件事:在任何時候贰镣,只在一個事務下執(zhí)行一個連接。這回答了第一個問題膳凝。
關(guān)于第二個問題碑隆,一般來說,一個事務的生命周期和語句差不多蹬音,你也可以手動結(jié)束它上煤。默認情況下劫狠,事務自動提交,當然你也可以通過BEGIN..COMMIT手動提交。接下來的問題
是事務如何與鎖關(guān)聯(lián)荞膘。

鎖的狀態(tài)

大多數(shù)情況下淘菩,鎖的生命周期在事務的生命周期之中。它們不一定同時開始进陡,但總時同時結(jié)束。當你結(jié)束一個事務時,也會釋放它相關(guān)的鎖赡磅。或者說,鎖直到事務結(jié)束或系統(tǒng)崩潰時才會釋放。如果系統(tǒng)在事務沒有結(jié)束的情況下崩潰灸眼,那么下一個訪問數(shù)據(jù)庫的連接會處理這種情況捕仔。

讀事務

我們先來看看SELECT語句執(zhí)行時鎖的狀態(tài)變化過程闸天,非常簡單:一個連接執(zhí)行SELECT語句,觸發(fā)一個事務,從UNLOCKED到SHARED贷帮,當事務COMMIT時,又回到UNLOCKED,就這么簡單沃但。
那么,當你運行兩個語句時會發(fā)生什么呢?這時如何加鎖呢?這依賴于你是否運行在自動提交狀態(tài)耕陷。考慮下面的例子(為了簡單猾警,這里用了偽碼):
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命令在一個事務下執(zhí)行。第一個exec()執(zhí)行時玄货,連接處于SHARED馆里,然后第二個exec()執(zhí)行以舒。當事務提交時卵贱,連接又從
SHARED回到UNLOCKED狀態(tài),狀態(tài)變化如下:

UNLOCKED→PENDING→SHARED→UNLOCKED
如果沒有BEGIN和COMMIT兩行缀辩,兩個SELECT都運行于自動提交狀態(tài)畅蹂,狀態(tài)變化如下:
UNLOCKED→PENDING→SHARED→UNLOCKED→PENDING→SHARED→UNLOCKED
僅僅是讀數(shù)據(jù),但在自動提交模式下,卻會經(jīng)歷兩個加解鎖的循環(huán),太麻煩烛谊。而且双泪,一個寫進程可能插到兩個SELECT中間對數(shù)據(jù)庫進行修改,這樣,你就不能保證第二次能夠讀到
同樣的數(shù)據(jù)了,而使用BEGIN..COMMIT就可以有此保證。

寫事務

下面我們來考慮寫數(shù)據(jù)庫毅待,比如UPDATE。和讀事務一樣鳖链,它也會經(jīng)歷UNLOCKED→PENDING→SHARED的變化過程灌侣,但接下來就會看到PENDING鎖是如何起到關(guān)口作用的了痊乾。

保留(RESERVED)狀態(tài)

當一個連接(connection)要向數(shù)據(jù)庫寫數(shù)據(jù)時,從SHARED狀態(tài)變?yōu)镽ESERVED狀態(tài)而线。如果它得到RESERVED鎖嘹狞,也就意味著它已經(jīng)準備好進行寫操作了。即使它沒有把修改寫入數(shù)據(jù)庫誓竿,也可以把修改保存到位于pager的緩沖區(qū)中(page cache)了磅网。
當一個連接進入RESERVED狀態(tài),pager就開始初始化回卷日志(rollback journal)筷屡〗担回卷日志是一個文件簸喂,用于回卷和崩潰恢復,見圖5-1燎潮。在RESERVED狀態(tài)下喻鳄,pager管理著三種頁:
(1)已修改的頁:包含被B-tree修改的記錄,位于page cache中确封。
(2)未修改的頁:包含沒有被B-tree修改的記錄除呵。
(3)日志頁:這是修改頁以前的版本,日志頁并不存儲在page cache中爪喘,而是在B-tree修改頁之前寫入日志颜曾。
Page cache非常重要,正是因為它的存在腥放,一個處于RESERVED狀態(tài)的連接可以真正的開始工作泛啸,而不會干擾其它的(讀)連接。所以秃症,SQLite可以高效地處理在同一時刻的多個讀連接和一個寫連接。

未決(UNPENDING)狀態(tài)

當一個連接完成修改吕粹,需要真正開始提交事務時种柑,執(zhí)行該過程的pager進入EXCLUSIVE狀態(tài)。從RESERVED狀態(tài)開始匹耕,pager試著獲取PENDING鎖聚请,一旦得到,就獨占它稳其,不允
許任何其它連接獲得PENDING鎖驶赏。既然寫操作持有PENDING鎖,其它任何連接都不能從UNLOCKED狀態(tài)進入SHARED狀態(tài)既鞠,即不會再有新的讀進程煤傍,也不會再有新的寫進程。
只有那些已經(jīng)處于SHARED狀態(tài)的連接可以繼續(xù)工作嘱蛋。而處于PENDING狀態(tài)的寫進程會一直等到所有這些連接釋放它們的鎖蚯姆,然后對數(shù)據(jù)庫加EXCUSIVE鎖,進入EXCLUSIVE
狀態(tài)洒敏,獨占數(shù)據(jù)庫龄恋。

排它(EXCLUSIVE)狀態(tài)

在EXCLUSIVE狀態(tài)下,主要的工作是把修改的頁從page cache寫入數(shù)據(jù)庫文件凶伙,這是真正進行寫操作的地方郭毕。在pager將修改頁寫到文件之前,還必須先處理日志函荣。它檢查是否所有的日志都寫入了磁盤显押,因為它們可能還位于操作系統(tǒng)的緩沖區(qū)中扳肛。所以pager得告訴OS把所有的文件寫入磁盤,這與synchronous pragma所做的工作相同煮落,如第4章所述敞峭。
日志是數(shù)據(jù)庫進行恢復的惟一方法,所以日志對于DBMS非常重要蝉仇。如果日志頁沒有完全寫入磁盤而發(fā)生崩潰旋讹,數(shù)據(jù)庫就不能恢復到它原來的狀態(tài),此時數(shù)據(jù)庫就處于不一致狀態(tài)轿衔。
日志寫盤完成后沉迹,pager就把所有的修改頁寫入數(shù)據(jù)庫文件。接下來做什么取決于事務提交的模式害驹,如果是自動提交鞭呕,那么pager清理日志、page cache宛官,然后由EXCLUSIVE進入
UNLOCKED葫松。如果是手動提交,那么pager繼續(xù)持有EXCLUSIVE鎖和回卷日志底洗,直至遇到COMMIT或者ROLLBACK腋么。
總之,出于性能方面的考慮亥揖,進程占有排它鎖的時間應該盡可能的短珊擂,所以DBMS通常都是在真正寫文件時才會占有排它鎖,這樣能大大提高并發(fā)性能费变。

1.什么是WAL摧扇?

WAL的全稱是Write Ahead Logging,它是很多數(shù)據(jù)庫中用于實現(xiàn)原子事務的一種機制挚歧,SQLite在3.7.0版本引入了該特性扛稽。

2.WAL如何工作?

在引入WAL機制之前昼激,SQLite使用rollback journal機制實現(xiàn)原子事務庇绽。

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

WAL機制的原理是:修改并不直接寫入到數(shù)據(jù)庫文件中明未,而是寫入到另外一個稱為WAL的文件中;如果事務失敗壹蔓,WAL中的記錄會被忽略趟妥,撤銷修改;如果事務成功佣蓉,它將在隨后的某個時間被寫回到數(shù)據(jù)庫文件中披摄,提交修改。

同步WAL文件和數(shù)據(jù)庫文件的行為被稱為checkpoint(檢查點)勇凭,它由SQLite自動執(zhí)行疚膊,默認是在WAL文件積累到1000頁修改的時候;當然虾标,在適當?shù)臅r候寓盗,也可以手動執(zhí)行checkpoint,SQLite提供了相關(guān)的接口璧函。執(zhí)行checkpoint之后傀蚌,WAL文件會被清空。

在讀的時候蘸吓,SQLite將在WAL文件中搜索喳张,找到最后一個寫入點,記住它美澳,并忽略在此之后的寫入點(這保證了讀寫和讀讀可以并行執(zhí)行);隨后摸航,它確定所要讀的數(shù)據(jù)所在頁是否在WAL文件中制跟,如果在,則讀WAL文件中的數(shù)據(jù)酱虎,如果不在雨膨,則直接讀數(shù)據(jù)庫文件中的數(shù)據(jù)。

在寫的時候读串,SQLite將之寫入到WAL文件中即可聊记,但是必須保證獨占寫入,因此寫寫之間不能并行執(zhí)行恢暖。

WAL在實現(xiàn)的過程中排监,使用了共享內(nèi)存技術(shù),因此杰捂,所有的讀寫進程必須在同一個機器上舆床,否則,無法保證數(shù)據(jù)一致性。

3.WAL的優(yōu)點與缺點

優(yōu)點:

  1. 讀和寫可以完全地并發(fā)執(zhí)行挨队,不會互相阻塞(但是寫之間仍然不能并發(fā))谷暮。

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

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

缺點:

1.訪問數(shù)據(jù)庫的所有程序必須在同一主機上,且支持共享內(nèi)存技術(shù)腾夯。

2.每個數(shù)據(jù)庫現(xiàn)在對應3個文件:<yourdb>.db颊埃,<yourdb>-wal,<yourdb>-shm俯在。

3.當寫入數(shù)據(jù)達到GB級的時候竟秫,數(shù)據(jù)庫性能將下降。

4.3.7.0之前的SQLite無法識別啟用了WAL機制的數(shù)據(jù)庫文件跷乐。

4.WAL引入的兼容性問題

在啟用了WAL之后肥败,數(shù)據(jù)庫文件格式的版本號由1升級到了2,因此愕提,3.7.0之前的SQLite無法識別啟用了WAL機制的數(shù)據(jù)庫文件馒稍。

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

5.WAL引入的性能問題

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

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

2.當checkpoint執(zhí)行的時候,會降低當時的讀寫性能灶似,因此列林,WAL可能會導致周期性的性能下降。

6.與WAL相關(guān)的PRAGMA和接口

PRAGMA journal_mode

PRAGMA wal_checkpoint

PRAGMA wal_autocheckpoint

sqlite3_wal_checkpoint

sqlite3_wal_autocheckpoint

sqlite3_wal_hook

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末酪惭,一起剝皮案震驚了整個濱河市希痴,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌撞蚕,老刑警劉巖润梯,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡纺铭,警方通過查閱死者的電腦和手機寇钉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來舶赔,“玉大人扫倡,你說我怎么就攤上這事【鼓桑” “怎么了撵溃?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長锥累。 經(jīng)常有香客問我缘挑,道長,這世上最難降的妖魔是什么桶略? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任语淘,我火速辦了婚禮,結(jié)果婚禮上际歼,老公的妹妹穿的比我還像新娘惶翻。我一直安慰自己,他們只是感情好鹅心,可當我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布吕粗。 她就那樣靜靜地躺著,像睡著了一般旭愧。 火紅的嫁衣襯著肌膚如雪颅筋。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天输枯,我揣著相機與錄音垃沦,去河邊找鬼。 笑死用押,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的靶剑。 我是一名探鬼主播蜻拨,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼桩引!你這毒婦竟也來了缎讼?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤坑匠,失蹤者是張志新(化名)和其女友劉穎血崭,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡夹纫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年咽瓷,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片舰讹。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡茅姜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出月匣,到底是詐尸還是另有隱情钻洒,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布锄开,位于F島的核電站素标,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏萍悴。R本人自食惡果不足惜头遭,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望退腥。 院中可真熱鬧任岸,春花似錦、人聲如沸狡刘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嗅蔬。三九已至剑按,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間澜术,已是汗流浹背艺蝴。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留鸟废,地道東北人猜敢。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像盒延,于是被迫代替她去往敵國和親缩擂。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內(nèi)容