iOS之sqlite和FMDB,iOS之sqliteFMDB

來源:互聯網
上載者:User

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/

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.