文章目录
  1. 1. 想法
  2. 2. 总结

多线程访问数据库本身就存在分险,容易形成脏数据。幸好 FMDB 这个第三方库支持了多线程访问,从而解决了脏数据问题。然而也带来了死锁问题……

先看看FMDB的多线程机制的原理。

看了它的代码,发现其实很简单的一个思路,但是实现起来还真不容易!!它是生成一个请求队列,将一次事务进行封装放在队列中,只有一个事务完成了,才会进行下一个事务。这就出现了一种现象:如果某一个请求耗时过大,将会导致所有请求堵塞!!同时也要求:同一个线程中,在一个事务完成之前,不能进行下一个事务。

举个例子:

1
2
3
4
5
self.dbQueue inTransaction:^(FMDatabase *db, BOOL *rollback) {
if ([db executeUpdate:sql]) {
[self increaseCounter];
}
}];

这个方法在执行sql成功后,将会调用increaseCounter方法:

1
2
3
4
5
- (void)increaseCounter {
[self.dbQueue inTransaction:^(FMDatabase *db, BOOL *rollback) {
[db executeQuery:@"insert ..."];
}];
}

而increaseCounter方法中又增加一个事务,去执行另外一个sql语句。

结果出现了这种现象:

第一个事务没结束,第二个事务等待第一个事务结束,而第一个事务等待第二个事务返回状态,出现死锁!!

正确的写法:

1
2
3
4
5
6
7
__block BOOL status = NO;
[self.dbQueue inTransaction:^(FMDatabase *db, BOOL *rollback) {
status = [db executeUpdate:sql];
}];
if (status) {
[self increaseCounter];
}

也就是将两个事务拆开。

想法

在App中保持一个FMDatabaseQueue的实例,并在所有的线程中都只使用这一个实例。

1
[FMDatabaseQueue databaseQueueWithPath:path];

FMDatabaseQueue 虽然看似一个队列,实际上它本身并不是,它通过内部创建一个Serial的dispatch_queue_t 来处理通过 inDatabaseinTransaction 传入的Blocks,所以当我们在主线程(或者后台)调用 inDatabase 或者 inTransaction 时,代码实际上是同步的。FMDatabaseQueue 这么设计的目的是让我们避免发生并发访问数据库的问题,因为对数据库的访问可能是随机的(在任何时候)、不同线程间(不同的网络回调等)的请求。内置一个Serial队列后,FMDatabaseQueue 就变成线程安全了,所有的数据库访问都是同步执行,而且这比使用@synchronizedNSLock 要高效得多。

但是这么一来就有了一个问题:如果后台在执行大量的更新,而主线程也需要访问数据库,虽然要访问的数据量很少,但是在后台执行完之前,还是会阻塞主线程。

对此,robertmryan 给出了一些想法:

  1. 如果你是在后台使用的 inDatabase 来执行更新,可以考虑换成 inTransaction,后者比前者更新起来快很多,特别是在更新量比较大的时候(比如更新1000条或10000条)。
  2. 拆解你的更新数据量,如果有300条,可以分10次、每次更新30条。当然有时不能这么做,因为你可能通过网络请求回来的数据,你希望一次性、完整地写入到数据库中,虽然有局限性,不过这确实能很好地减少每个Block占用数据库的时间。
  3. 上面两点可以改善问题,但是问题依然是存在的,在大多数时候,你应该把从主线程调用inDatabaseinTransaction 放在异步里:
1
2
3
4
5
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self.databaseQueue inDatabase:^(FMDatabase *db) {
//do something...
}];
});

这种方式能解决不依赖于数据库返回的结果的情况,如果对返回结果有依赖,就需要考虑UI上的体验了,如加一个 UIActivityIndicatorView

总结

  • 多线程编程应该尽量采用事务队列
  • 事务队列与Mysql等的锁数据库效果相似,即事务完成前,其他线程以及本线程不能访问数据库(注意不是锁表,是锁库
  • 一个事务内应该只包含数据库请求语句,不应当包含其他逻辑
  • 一个请求不应耗时过长,否则出现线程等待