数据库 sqlite 在 iOS 中起着举足轻重的作用,本文主要讲述一下 sqlite 的并发,事务和常见的损坏问题,后面会简述一下对 sqlite 进一步封装的第三方库 FMDB。
在了解 sqlite 的事务和并发之前,我们要先了解 sqlite 提供的几种锁的类型及区别。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_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 无法解决这种问题。
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;
- }
初始化方法,我们捡重要的说:
使用得时候,会调用这个方法:
- - (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/
来源: http://www.cnblogs.com/lizheng114/p/7401022.html