使用FMDB多线程访问数据库 及databaseislocked的问题

  • 今天终于解决了多线程同时访问数据库时,报数据库锁定的问题,错误信息是:

    Unknown error finalizing or resetting statement (5: database is locked)

    最后通过FMDatabaseQueue解决了这个问题,本文总结一下:

    FMDatabase不能多线程使用同一个实例

    多线程访问数据库,不能使用同一个FMDatabase的实例,否则会发生异常。如果线程使用单独的FMDatabase实例是允许的,但是同样有可能发生database is locked的问题。这是由于多线程对sqlite的竞争引起的

    我的app一开始就是多线程使用单独的FMDatabase实例访问数据库,虽然没有引起crash,但是还是出现了database is locked问题,造成很多数据没有如预期写入数据库

    使用FMDatabaseQueue,问题依旧

    后来上FMDB的官网看了文档,确认用FMDatabaseQueue可以解决这个问题,API也比较简单:

    1. NSString *dbFilePath = [PathResolver databaseFilePath];
    2. queue = [FMDatabaseQueue databaseQueueWithPath:dbFilePath];
    3. [queue inDatabase:^(FMDatabase *db){
    4. // access db
    5. }];
    但是实际测试了一下,还是database is locked

    读了一下相关的源码,FMDatabaseQueue解决这个问题的思路是:创建一个队列,然后将放入队列的block顺序执行,这样避免了多线程同时访问数据库

    而我的代码是多线程各创建FMDatabaseQueue的实例,所以其实有多个队列,因此还是存在数据库竞争的问题,和用FMDatabase时是一样的

    共享同一个FMDatabaseQueue实例

    于是接下来我让每个线程使用同一个Queue实例,问题就顺利解决了

    实现的方式,一开始我想给FMDatabase增加一个单例方法,但是这样以后升级FMDB会比较麻烦,所以最后我是创建了一个Helper类

    01. @implementation LosDatabaseHelper
    02.  
    03. {
    04. FMDatabaseQueue* queue;
    05. }
    06.  
    07. -(id) init
    08. {
    09. self = [super init];
    10. if(self){
    11. NSString *dbFilePath = [PathResolver databaseFilePath];
    12. queue = [FMDatabaseQueue databaseQueueWithPath:dbFilePath];
    13. }
    14. return self;
    15. }
    16.  
    17. +(LosDatabaseHelper*) sharedInstance
    18. {
    19. static dispatch_once_t pred = 0;
    20. __strong static id _sharedObject = nil;
    21. dispatch_once(&pred, ^{
    22. _sharedObject = [[self alloc] init];
    23. });
    24. return _sharedObject;
    25. }
    26.  
    27. -(void) inDatabase:(void(^)(FMDatabase*))block
    28. {
    29. [queue inDatabase:^(FMDatabase *db){
    30. block(db);
    31. }];
    32. }
    33.  
    34. @end

    系统中其他的类,使用这个Helper类的单例,这样保证了全局只有唯一的FMDatabaseQueue实例。注意,因为Helper内部持有的是FMDatabaseQueue,所以可以这么做,如果包装的是FMDatabase类,就绝对会有问题。因为FMDatabase实例不能在多线程环境共享

    使用FMDatabaseQueue之后,管理db

    原本使用FMDatabase类,需要手工调用db的open和close方法

    但是用FMDatabaseQueue,不需要调用open,因为查看代码发现,Queue已经open了。至于要不要close,我也不确定,因为官方的sample code没有调用close。实际应用中,我也没有调用,好像没有问题。如果需要close的话,我想可以在Helper类的公共方法里增加调用close queue就可以了。下面是close的源码:

    01. - (void)close {
    02. FMDBRetain(self);
    03. dispatch_sync(_queue, ^() { 
    04. [_db close];
    05. FMDBRelease(_db);
    06. _db = 0x00;
    07. });
    08. FMDBRelease(self);
    09. }

    所以,使用Queue,是不需要自己打开和关闭db的。但是如果使用了FMResultSet,rs倒是需要关闭,否则会报warning:

    1. if ([db hasOpenResultSets]) {
    2. NSLog(@"Warning: there is at least one open result set around after performing [FMDatabaseQueue inDatabase:]");

    为了不看到warning,我都在block里调用了[rs close]

    刷新数据库文件路径

    具体到我们的应用,还有一个特殊问题需要考虑。因为我们的APP可以切换账户,而账户的db文件是独立的。所以当用户重新登录的时候,需要刷新一下Helper的queue

    01. +(void) refreshDatabaseFile
    02. {
    03. LosDatabaseHelper *instance = [self sharedInstance];
    04. [instance doRefresh];
    05. }
    06.  
    07. -(void) doRefresh
    08. {
    09. NSString *dbFilePath = [PathResolver databaseFilePath];
    10. queue = [FMDatabaseQueue databaseQueueWithPath:dbFilePath];
    11. }

    如果不这么做,由于Helper是单例,那么切换账户以后,用户B访问的还是用户A的数据库。刷新的调用,一般放在登录之后,进入主页面之前就可以了

    队列和线程

    在debug过程中,顺便看到一个现象。虽然多个block都是放到同一个队列里,但是其实是跑在不同的thread里

    不要混淆队列和线程的概念,使用GCD时,开发者关注的是把block放到队列中,但是同一个队列其实可以对应多个thread,为block分配thread,是GCD框架负责的,开发者不需要关注。只要把操作放到合适的队列里,GCD就会完成线程的创建,分配与回收

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值