iOS之sqlite和FMDB


數據庫sqlite在iOS中起着舉足輕重的作用,本文主要講述一下sqlite的並發,事務和常見的損壞問題,后面會簡述一下對sqlite進一步封裝的第三方庫FMDB。

sqlite的並發和事務

在了解sqlite的事務和並發之前,我們要先了解sqlite提供的幾種鎖的類型及區別。sqlite提供了五種級別的鎖:

  1. UNLOCKED(未鎖定):當前數據庫不存在讀寫操作。為默認狀態。
  2. SHARED(共享鎖):當前數據庫可以被讀取,但是不能執行寫操作。同一時刻,可以有任意的進程持有該數據庫的共享鎖,所以sqlite的讀操作是並發的。一旦共享鎖處於活動狀態,其他的寫操作都不能進行,必須等待。
  3. RESERVED(保留鎖):表示當前有進程計划在未來的某一時刻對數據庫執行寫操作,但是因為仍然有其他進程持有共享鎖在讀取數據。同一時刻,同一數據庫只能有一個保留鎖和多個共享鎖。
  4. PENDING(待定鎖):表示持有該待定鎖的進程即將開始對數據庫執行寫操作,但是需要等待共享鎖讀取數據完畢,以便於獲取EXCLUSIVE(排他鎖)。此時,其他的讀操作將不被允許,這樣就很好的解決了寫飢餓的問題(如果總是有讀的操作存在,那么寫操作就不能執行)。
  5. EXCLUSIVE(排他鎖):獲取排他鎖后,數據庫將不再接受其他鎖,直到寫操作完成。所以sqlite為了提升並發性,會在盡量短的時間內處理完。

由以上5種鎖的機制,我們可以看出,sqlite對於讀操作是可以很好的支持並發的,但是對於寫操作,因為他采用的是鎖庫的方式,所以其寫操作的並發性會受到很大影響。而且比較容易產生死鎖。

數據庫的事務主要用於保證數據操作的原子性,一致性,隔離性,而且可以統一回滾和提交事務。

sqlite下的sql默認都處於自動提交的模式下,但是一旦聲明了 “Begin Transaction”,則表示要將模式改為手動提交。

begin transaction
select * 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的一系列操作,具體包含:

  1. FMResultSet : 表示FMDatabase執行查詢之后的結果集和一些操作。

  2. FMDatabase : 表示一個單獨的SQLite數據庫操作實例,通過它可以對數據庫進行增刪改查等等操作。

  3. FMDatabaseAdditions : 擴展FMDatabase類,新增對查詢結果只返回單個值的方法進行簡化,對表、列是否存在,版本號,校驗SQL等等功能。

  4. FMDatabaseQueue : 對多線程的操作數據庫進行了支持。

  5. FMDatabasePool : 使用任務池的形式,對多線程的操作提供支持。(官方不推薦使用)

 我們主要講解一下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/


免責聲明!

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



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