彻底解决iOS数据库崩溃:FMDatabaseQueue多线程安全原理与实战

彻底解决iOS数据库崩溃:FMDatabaseQueue多线程安全原理与实战

【免费下载链接】fmdb ccgus/fmdb: 是一个 iOS 的SQLite 数据库框架。适合用于iOS 开发中的数据存储和管理。 【免费下载链接】fmdb 项目地址: https://gitcode.com/gh_mirrors/fm/fmdb

你是否曾因iOS应用中的SQLite数据库崩溃而彻夜难眠?当用户投诉"数据丢失"时,你是否怀疑过是多线程操作数据库导致的问题?本文将从源码层面解析FMDB框架中FMDatabaseQueue如何保障多线程安全,并通过实战案例展示如何正确使用这一核心组件。读完本文,你将彻底理解数据库线程安全的实现机制,掌握避免常见崩溃的实战技巧。

FMDatabaseQueue核心原理

线程安全的本质:序列化队列

FMDatabaseQueue的核心设计思想是通过GCD串行队列(Dispatch Queue)实现所有数据库操作的序列化执行。这一机制确保同一时刻只有一个线程访问数据库,从根本上消除了多线程并发导致的资源竞争问题。

src/fmdb/FMDatabaseQueue.m中,我们可以看到队列的创建过程:

_queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL);
dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);

这里创建了一个串行队列,并通过dispatch_queue_set_specific将队列与当前FMDatabaseQueue实例关联,用于后续的死锁检测。

数据库连接管理

FMDatabaseQueue维护了一个数据库连接池,在src/fmdb/FMDatabaseQueue.mdatabase方法中实现了数据库连接的自动管理:

- (FMDatabase*)database {
    if (![_db isOpen]) {
        if (!_db) {
           _db = FMDBReturnRetained([[[self class] databaseClass] databaseWithPath:_path]);
        }
        
#if SQLITE_VERSION_NUMBER >= 3005000
        BOOL success = [_db openWithFlags:_openFlags vfs:_vfsName];
#else
        BOOL success = [_db open];
#endif
        if (!success) {
            NSLog(@"FMDatabaseQueue could not reopen database for path %@", _path);
            FMDBRelease(_db);
            _db  = 0x00;
            return 0x00;
        }
    }
    
    return _db;
}

这段代码确保在每次数据库操作前,连接都处于打开状态,如果连接丢失会自动尝试重新打开。

死锁防护机制

FMDatabaseQueue在src/fmdb/FMDatabaseQueue.m中实现了死锁防护:

#ifndef NDEBUG
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");
#endif

这段代码通过dispatch_get_specific检查当前是否已经在队列中执行操作,如果是则触发断言,防止因嵌套调用导致的死锁。

核心API解析

初始化与创建

FMDatabaseQueue提供了多种初始化方法,最常用的是通过路径创建队列:

// 初始化队列
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath];

对应src/fmdb/FMDatabaseQueue.h中的接口定义:

+ (nullable instancetype)databaseQueueWithPath:(NSString * _Nullable)aPath;

你也可以通过URL初始化,并指定打开标志和虚拟文件系统:

+ (nullable instancetype)databaseQueueWithURL:(NSURL * _Nullable)url flags:(int)openFlags;
- (nullable instancetype)initWithPath:(NSString * _Nullable)aPath flags:(int)openFlags vfs:(NSString * _Nullable)vfsName;

基本数据库操作

使用inDatabase:方法执行数据库操作,所有操作都会在串行队列中顺序执行:

[queue inDatabase:^(FMDatabase *db) {
    [db executeUpdate:@"INSERT INTO myTable VALUES (?)", @1];
    FMResultSet *rs = [db executeQuery:@"SELECT * FROM myTable"];
    while ([rs next]) {
        // 处理结果
    }
}];

对应src/fmdb/FMDatabaseQueue.h中的方法定义:

- (void)inDatabase:(__attribute__((noescape)) void (^)(FMDatabase *db))block;

事务操作

FMDatabaseQueue提供了事务支持,确保一系列操作的原子性:

[queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
    [db executeUpdate:@"INSERT INTO myTable VALUES (?)", @1];
    [db executeUpdate:@"INSERT INTO myTable VALUES (?)", @2];
    
    if (someErrorOccurred) {
        *rollback = YES;
        return;
    }
    
    [db executeUpdate:@"INSERT INTO myTable VALUES (?)", @3];
}];

src/fmdb/FMDatabaseQueue.h中定义了事务相关的多个方法:

- (void)inTransaction:(__attribute__((noescape)) void (^)(FMDatabase *db, BOOL *rollback))block;
- (void)inDeferredTransaction:(__attribute__((noescape)) void (^)(FMDatabase *db, BOOL *rollback))block;
- (void)inExclusiveTransaction:(__attribute__((noescape)) void (^)(FMDatabase *db, BOOL *rollback))block;
- (void)inImmediateTransaction:(__attribute__((noescape)) void (^)(FMDatabase * _Nonnull, BOOL * _Nonnull))block;

保存点操作

对于更细粒度的事务控制,可以使用保存点(Save Point)功能:

NSError *error = [queue inSavePoint:^(FMDatabase *db, BOOL *rollback) {
    [db executeUpdate:@"INSERT INTO myTable VALUES (?)", @1];
    if (errorOccurred) {
        *rollback = YES;
        return;
    }
}];
if (error) {
    // 处理错误
}

实战案例分析

多线程写入冲突测试

Tests/FMDatabaseQueueTests.m中,有一个压力测试案例,模拟了多线程并发写入的场景:

- (void)testStressTest
{
    size_t ops = 16;
    
    dispatch_queue_t dqueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    dispatch_apply(ops, dqueue, ^(size_t nby) {
        
        // 模拟并发操作
        if (nby % 2 == 1) {
            [NSThread sleepForTimeInterval:.01];
            
            [self.queue inTransaction:^(FMDatabase *adb, BOOL *rollback) {
                FMResultSet *rsl = [adb executeQuery:@"select * from qfoo where foo like 'h%'"];
                while ([rsl next]) {
                    ;// 处理结果
                }
            }];
        }
        
        if (nby % 3 == 1) {
            [NSThread sleepForTimeInterval:.01];
        }
        
        [self.queue inTransaction:^(FMDatabase *adb, BOOL *rollback) {
            XCTAssertTrue([adb executeUpdate:@"insert into qfoo values ('1')"]);
            XCTAssertTrue([adb executeUpdate:@"insert into qfoo values ('2')"]);
            XCTAssertTrue([adb executeUpdate:@"insert into qfoo values ('3')"]);
        }];
    });
}

这个测试通过dispatch_apply在多个线程同时发起数据库操作,但由于FMDatabaseQueue的串行化处理,所有操作都能安全执行而不会导致数据库损坏。

事务回滚测试

Tests/FMDatabaseQueueTests.m中的事务回滚测试展示了如何正确使用事务确保数据一致性:

- (void)testTransaction
{
    [self.queue inDatabase:^(FMDatabase *adb) {
        [adb executeUpdate:@"create table transtest (a integer)"];
        XCTAssertTrue([adb executeUpdate:@"insert into transtest values (1)"]);
        XCTAssertTrue([adb executeUpdate:@"insert into transtest values (2)"]);
        
        int rowCount = 0;
        FMResultSet *ars = [adb executeQuery:@"select * from transtest"];
        while ([ars next]) {
            rowCount++;
        }
        
        XCTAssertEqual(rowCount, 2);
    }];
    
    [self.queue inTransaction:^(FMDatabase *adb, BOOL *rollback) {
        XCTAssertTrue([adb executeUpdate:@"insert into transtest values (3)"]);
        
        if (YES) { // 模拟错误情况
            *rollback = YES;
            return;
        }
        
        XCTFail(@"This shouldn't be reached");
    }];
    
    [self.queue inDatabase:^(FMDatabase *adb) {
        
        int rowCount = 0;
        FMResultSet *ars = [adb executeQuery:@"select * from transtest"];
        while ([ars next]) {
            rowCount++;
        }
        
        XCTAssertFalse([adb hasOpenResultSets]);
        
        XCTAssertEqual(rowCount, 2); // 验证回滚成功,行数仍为2
    }];
}

常见错误案例:多队列竞争

最常见的错误是同时创建多个FMDatabaseQueue实例操作同一个数据库文件:

// 错误示例!不要这样做!
FMDatabaseQueue *queue1 = [FMDatabaseQueue databaseQueueWithPath:path];
FMDatabaseQueue *queue2 = [FMDatabaseQueue databaseQueueWithPath:path];

// 这两个队列会独立操作数据库,导致竞争条件
[queue1 inDatabase:^(FMDatabase *db) { /* ... */ }];
[queue2 inDatabase:^(FMDatabase *db) { /* ... */ }];

正确的做法是为每个数据库文件只创建一个FMDatabaseQueue实例,并在整个应用中共享使用。

最佳实践与性能优化

单例模式管理队列

推荐使用单例模式管理FMDatabaseQueue实例,确保全局唯一:

@interface DBManager : NSObject
@property (nonatomic, strong, readonly) FMDatabaseQueue *queue;
+ (instancetype)sharedManager;
@end

@implementation DBManager
+ (instancetype)sharedManager {
    static DBManager *instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[DBManager alloc] init];
        NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"mydb.sqlite"];
        instance->_queue = [FMDatabaseQueue databaseQueueWithPath:path];
    });
    return instance;
}
@end

批量操作优化

对于大量数据插入,使用事务可以显著提升性能:

[[DBManager sharedManager].queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
    for (NSDictionary *data in largeDataArray) {
        BOOL success = [db executeUpdate:@"INSERT INTO myTable (id, name) VALUES (?, ?)", 
                       data[@"id"], data[@"name"]];
        if (!success) {
            *rollback = YES;
            break;
        }
    }
}];

避免长时间阻塞

不要在数据库操作中执行耗时任务,这会阻塞整个队列:

// 错误示例!不要这样做!
[queue inDatabase:^(FMDatabase *db) {
    // 数据库操作...
    
    // 长时间任务会阻塞队列
    [self performLongRunningTask];
    
    // 更多数据库操作...
}];

总结与注意事项

FMDatabaseQueue通过GCD串行队列实现了数据库操作的序列化,从根本上解决了SQLite的多线程安全问题。在使用过程中,需特别注意以下几点:

  1. 每个数据库文件只创建一个FMDatabaseQueue实例
  2. 避免在数据库操作块中执行耗时任务
  3. 正确使用事务和保存点确保数据一致性
  4. 不要在主线程执行大量数据操作,避免UI卡顿

官方测试用例Tests/FMDatabaseQueueTests.m包含了更多边界情况的处理示例,建议深入阅读以全面了解各种使用场景。

通过遵循本文介绍的原理和最佳实践,你可以构建一个稳定、高效的iOS数据库层,彻底告别数据库崩溃和数据丢失的烦恼。

【免费下载链接】fmdb ccgus/fmdb: 是一个 iOS 的SQLite 数据库框架。适合用于iOS 开发中的数据存储和管理。 【免费下载链接】fmdb 项目地址: https://gitcode.com/gh_mirrors/fm/fmdb

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值