iOS之sqlite和FMDB,iOS之sqliteFMDB
資料庫sqlite在iOS中起著舉足輕重的作用,本文主要講述一下sqlite的並發,事務和常見的損壞問題,後面會簡述一下對sqlite進一步封裝的第三方庫FMDB。
sqlite的並發和事務
在瞭解sqlite的事務和並發之前,我們要先瞭解sqlite提供的幾種鎖的類型及區別。sqlite提供了五種層級的鎖:
由以上5種鎖的機制,我們可以看出,sqlite對於讀操作是可以很好的支援並發的,但是對於寫操作,因為他採用的是鎖庫的方式,所以其寫操作的並發性會受到很大影響。而且比較容易產生死結。
資料庫的事務主要用於保證資料操作的原子性,一致性,隔離性,而且可以統一復原和提交事務。
sqlite下的sql預設都處於自動認可的模式下,但是一旦聲明了 “Begin Transaction”,則表示要將模式改為手動提交。
begin transactionselect * from table where ...insert into table values (...)rollback transaction / commit
當執行到select的時候,擷取到共用鎖定執行讀取操作。當執行到insert或者update,delete的時候,將會擷取保留鎖,但是在commit以前,都不會擷取到獨佔鎖定來真正寫入資料。
執行到rollback或者commit的時候,也並不表示會真正寫資料,而是將手動模式改為自動模式,依舊按照自動模式的流程來處理寫資料或者讀資料。不過有一點不同的地方是,rollback會設定一個標識來告訴自動模式的處理流程,資料需要復原。
sqlite的事務分三種類別:BEGIN [ DEFERRED | IMMEDIATE | EXCLUSIVE ] TRANSACTION
DEFERRED:就是我們上面介紹的,begin開始時不擷取任何鎖,執到讀或寫的語句執行時才會擷取相應的鎖
IMMEDIATE:如果指定為這種類別,那麼事務會嘗試擷取RESERVED鎖,如果成功,則其他串連將不能寫資料庫,可以讀。同時,也會阻止其他事務來執行 begin immediate或者begin exclusive,否則就返回SQLITE_BUSY。原因在RESERVED鎖的時候就說過“同一時刻,同一資料庫只能有一個保留鎖和多個共用鎖定”。
EXCLUSIVE:與IMMEDIATE類似,會嘗試擷取EXCLUSIVE鎖。
sqlite常見的問題:
SQLITE_BUSY:通常都是因為鎖的衝突導致的,比如:一旦有進程持有RESERVED鎖後,其他進程想要再持有RESERVED鎖,就會報這個錯誤;或者有進程持有PENDING鎖,而其他進程想要再持有SHARED鎖,也會報這個錯誤。死結也會導致這個錯誤,如:一個進程A持有SHARED鎖,然後正要申請RESERVED鎖,另一個進程B持有RESERVED鎖,正要申請EXCLUSIVE鎖,此時A要等待B的RESERVED鎖,而B要等待A的SHARED鎖釋放,產生死結,詳見:https://sqlite.org/c3ref/busy_handler.html。
SQLITE_LOCKED(database is locked):來自官方的解釋是:如果你在同一個資料庫連接中來處理兩件不相容的事情,就會報此錯誤。比如:
db eval {SELECT rowid FROM ex1} { if {$rowid==10} { db eval {DROP TABLE ex1} ;# will give SQLITE_LOCKED error } }
官方解釋地址:http://sqlite.org/cvstrac/wiki?p=DatabaseIsLocked
資料庫損壞:簡單來說就是當系統準備寫資料到資料庫檔案中時崩潰了(app崩潰,斷電,殺進程等),這個時候記憶體中將要寫入的資料資訊丟失,那麼此時唯一能夠恢複資料的機會就是日誌,但是日誌也有可能被損壞,所以如果日誌也被損壞或者丟失了,那麼資料庫也被損壞了。官方解釋是這樣說的:sqlite在unix系統下使用系統提供的fsync()方法將資料寫入磁碟,但是這個函數並不是每次都能正確的工作,特別是對於一些便宜的磁碟。。這是作業系統的bug,sqlite無法解決這種問題。
FMDatabaseQueue源碼解析
FMDB是第三方開源庫,封裝了sqlite的一系列操作,具體包含:
我們主要講解一下FMDatabaseQueue這個類。
- (instancetype)initWithPath:(NSString*)aPath flags:(int)openFlags vfs:(NSString *)vfsName { self = [super init]; if (self != nil) { _db = [[[self class] databaseClass] databaseWithPath:aPath]; FMDBRetain(_db); #if SQLITE_VERSION_NUMBER >= 3005000 BOOL success = [_db openWithFlags:openFlags vfs:vfsName];#else BOOL success = [_db open];#endif if (!success) { NSLog(@"Could not create database queue for path %@", aPath); FMDBRelease(self); return 0x00; } _path = FMDBReturnRetained(aPath); _queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL); dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL); _openFlags = openFlags; } return self;}
初始化方法,我們撿重要的說:
- 建立一個串列隊列,之後的sql的操作都會放到這個隊列中。為什麼不使用效率更高的並行隊列呢?前面說過,因為sqlite對寫操作是鎖庫,所以如果使用並行隊列,那麼會很容易返回SQL_BUSY錯誤。
- 為當前的隊列產生一個標識,用於以後在執行sql的時候來判斷是否同一隊列。
使用得時候,會調用這個方法:
- (void)inDatabase:(void (^)(FMDatabase *db))block { /* Get the currently executing queue (which should probably be nil, but in theory could be another DB queue * and then check it against self to make sure we're not about to deadlock. */ FMDatabaseQueue *currentSyncQueue = (__bridge id)dispatch_get_specific(kDispatchQueueSpecificKey); assert(currentSyncQueue != self && "inDatabase: was called reentrantly on the same queue, which would lead to a deadlock"); FMDBRetain(self); dispatch_sync(_queue, ^() { FMDatabase *db = [self database]; block(db); if ([db hasOpenResultSets]) { NSLog(@"Warning: there is at least one open result set around after performing [FMDatabaseQueue inDatabase:]"); #if defined(DEBUG) && DEBUG NSSet *openSetCopy = FMDBReturnAutoreleased([[db valueForKey:@"_openResultSets"] copy]); for (NSValue *rsInWrappedInATastyValueMeal in openSetCopy) { FMResultSet *rs = (FMResultSet *)[rsInWrappedInATastyValueMeal pointerValue]; NSLog(@"query: '%@'", [rs query]); }#endif } }); FMDBRelease(self);}
首先會判斷是否是同一隊列,如果不是同一隊列,那麼容易發生死結的情況,理由就是:同一資料庫執行個體被不同的隊列持有,但是因為寫操作是鎖庫的,所以當兩個隊列都要寫庫和讀庫的時候,就容易發生死結的情況,詳情參看上面的SQLITE_BUSY的解釋。
然後使用dispatch_sync來同步處理隊列中的block,這裡可能會有疑問為什麼不使用diapatch_async來非同步處理呢?這涉及到同步串列隊列和非同步串列隊列的區別,區別在於同步會阻塞當前線程,非同步不會,相同點在於隊列中的任務都是一個接一個順序執行。這裡我預計是因為FMDB作者認為只需要提供同步方法就可以了,提供非同步方法呼叫會開啟新的線程,增大開銷,如果使用者有需要,在外面再套一層dispatch_async就行了。而且使用dispatch_sync則表示該方法是安全執行緒的。
當我們使用事務的時候,我們會使用:
- (void)inDeferredTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block { [self beginTransaction:YES withBlock:block];}- (void)inTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block { [self beginTransaction:NO withBlock:block];}
上面的方法inDeferredTransaction表明事務使用DEFERRED類別;inTransaction表明事務使用EXCLUSIVE類別,這兩種區別請參看上面的事務類別的解釋。
後面還提供了一個方法:
#if SQLITE_VERSION_NUMBER >= 3007000- (NSError*)inSavePoint:(void (^)(FMDatabase *db, BOOL *rollback))block
提供一個儲存可以復原的點,可以設定是否復原。沒用過。。。
總體來看,因為sqlite這個資料庫的鎖的特殊性,所以導致了FMDatabaseQueue來這樣設計,所以我們在使用的時候,對於同一個資料庫執行個體,要保證FMDatabaseQueue的唯一性。
在後續可以思考改進的地方在於,作者沒有建立兩個隊列,一個用來讀,一個用來寫,因為sqlite是支援讀共用的,所以是否可以考慮專門建立並行讀隊列,不過需要防止“寫饑餓”的產生。
參考連結:
https://www.sqlite.org/lockingv3.html
http://shanghaiseagull.com/index.php/tag/fmdb/