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/

时间: 2024-08-15 16:17:28

iOS之sqlite和FMDB的相关文章

iOS开发数据库篇—FMDB数据库队列(下)

iOS开发数据库篇—FMDB数据库队列(下) 一.代码示例 1.需要先导入FMDB框架和头文件,由于该框架依赖于libsqlite库,所以还应该导入该库. 2.代码如下: 1 // 2 // YYViewController.m 3 // 05-FMDB数据库队列 4 // 5 // Created by apple on 14-7-28. 6 // Copyright (c) 2014年 wendingding. All rights reserved. 7 // 8 9 #import "Y

[iOS]数据库第三方框架FMDB详细讲解

[iOS]数据库第三方框架FMDB详细讲解 初识FMDB iOS中原生的SQLite API在进行数据存储的时候,需要使用C语言中的函数,操作比较麻烦.于是,就出现了一系列将SQLite API进行封装的库,例如FMDB.PlausibleDatabase.sqlitepersistentobjects等. FMDB是一款简洁.易用的封装库.因此,在这里推荐使用第三方框架FMDB,它是对libsqlite3框架的封装,用起来的步骤与SQLite使用类似,并且它对于多线程的并发操作进行了处理,所以

IOS开发之数据库FMDB

IOS开发之数据库FMDB 1.简介 需求作用:如果需要保存大量的结构较为复杂的数据时候, 使用数据库, 例如交规考试项目 常用的数据库: (1)Microsoft SQL Server 2000/2008:中小企业使用较多 (2)Oracle:比较复杂, 大企业使用较多 (3)Mysql数据库:网站使用较多 (4)sqlite:本地数据库, 访问数据足够快, 直接访问文件  足够简单, 功能相对其他数据库软件不是特别齐全, 足够用了  足够小, 系统不超过1M, 适合在移动端上使用 2. Me

iOS开发数据库篇—FMDB简单介绍

iOS开发数据库篇—FMDB简单介绍 一.简单说明 1.什么是FMDB FMDB是iOS平台的SQLite数据库框架 FMDB以OC的方式封装了SQLite的C语言API 2.FMDB的优点 使用起来更加面向对象,省去了很多麻烦.冗余的C语言代码 对比苹果自带的Core Data框架,更加轻量级和灵活 提供了多线程安全的数据库操作方法,有效地防止数据混乱 3.FMDB的github地址 https://github.com/ccgus/fmdb 二.核心类 FMDB有三个主要的类 (1)FMDa

数据持久化-CoreData、SQLite、FMDB

1.CoreData 1.1 CoreData概述 1)Core data 是数据持久存储的最佳方式 2)Core Data 基于model-view-controller(mvc)模式下,为创建分解的cocoa应用程序提供了一个灵活和强大的数据模型框架. 3)Core Data可以是你以图形界面的方式快速的定义app的数据模型,同时在你的代码中容易获取到它. Core Data提供了基础结构去处理常用的功能,例如:保存,恢复,撤销和重做,允许你在app中继续创建新的任务.在使用 Core Da

iOS开发数据库篇—FMDB数据库队列

iOS开发数据库篇—FMDB数据库队列 一.代码示例 1.需要先导入FMDB框架和头文件,由于该框架依赖于libsqlite库,所以还应该导入该库. 2.代码如下: 1 // 2 // YYViewController.m 3 // 05-FMDB数据库队列 4 // 5 // Created by apple on 14-7-28. 6 // Copyright (c) 2014年 wendingding. All rights reserved. 7 // 8 9 #import "YYVi

iOS 数据库操作(使用FMDB)

iOS 数据库操作(使用FMDB) iOS中原生的SQLite API在使用上相当不友好,在使用时,非常不便.于是,就出现了一系列将SQLite API进行封装的库,例如FMDB.PlausibleDatabase.sqlitepersistentobjects等,FMDB (https://github.com/ccgus/fmdb) 是一款简洁.易用的封装库,这一篇文章简单介绍下FMDB的使用. 在FMDB下载文件后,工程中必须导入如下文件,并使用 libsqlite3.dylib 依赖包.

我为什么用 SQLite 和 FMDB 而不用 Core Data

转:http://segmentfault.com/a/1190000000363392 编者注:文章的"我"是指原作者. 凭良心讲,我不能告诉你不去使用Core Data.它不错,而且也在变好,并且它被很多其他Cocoa开发者所理解,当有新人加入你的组或者需要别人接手你的项目的时候,这点很重要.更重要的是,不值得花时间和精力去写自己的系统去代替它.真的,使用Core Data吧. 为什么我不使用Core Data Mike Ash写到: 就我自己而言,我不是个狂热粉丝.我发现API是

iOS数据持久化之数据库:SQLite和FMDB

SQLite: SQLite是一款轻量级型的数据库,资源占用少.性能良好和零管理成本,具有零配置(无需安装和管理配置).独立(没有额外依赖).储存在单一磁盘文件中的一个完整的数据库.源码完全的开源.比一些流行的数据库在大部分普通数据库操作要快……功能特性:在大型系统和处理大批量数据时不适用      SQLite引擎不是程序与之通信的独立进程,而是连接到程序中成为它的一个主要部分,所以主要的通信协议是在编程语言内的直接API调用:在APP开发中将SQLite集成到应用的沙盒目录下(SQLite是