数据库多线程
SQLite
多线程
三种线程模式:
单线程
Single-thread
在此模式下,所有互斥锁都被禁用,并且SQLite连接不能在多个线程中使用。
就是线程不安全的,不能在多线程使用
多线程
Multi-thread
在此模式下,SQLite可以安全地由多个线程使用,前提是在两个或多个线程中不
同时使用单个数据库连接。
多线程模式,但是要注意,此模式禁止多线程共用一个连接connect,会有问题
串行
Serialized
在此模式下,SQLite可以被多个线程安全地使用而没有任何限制。
串行队列,任务被逐个执行,线程安全
但是,串行队列效率低,不能同时异步读写
两种日志模式
DELETE模式和WAL模式,默认是DELETE模式
DELETE模式
日志文件记录的是数据页变更前的内容。
当事务开启时,将db-page的内容写入日志,写操作直接修改db-page,读操作
也是直接读取db-page,db-page存储了事务最新的所有更新,当事务提交时直
接删除日志文件即可,事务回滚时将日志文件覆盖db-page文件,恢复原始数
据。
WAL模式
WAL 日志模式优点就是,修改时是追加的,不影响同时读写,效率高,所以,在
多线程访问开启 WAL 的访问效率更高
日志文件记录的是数据变更后的内容。
当事务开启时,写操作【不直接修改】db-page,而是以append的方式追加到
日志文件末尾。
当事务提交时不会影响db-page,直接将日志文件覆盖到db-page即可,事务回
滚时直接将日志文件去掉即可。
读操作也是读取日志文件,开始读数据时会先扫描日志文件,看需要读的数据是
否在日志文件中,如果在直接读取,否则从对应的db-page读取,并引入.shm
文件,建立日志索引,采用哈希索引来加快日志扫描
Busy Retry 方案
SQLite 针对多线程场景下 任务堵塞时解决方案
SQLite提供了Busy Retry的方案,即发生阻塞时,会触发Busy Handler,此时可
以让等待的线程休眠一段时间后,重新尝试操作。重试一定次数依然失败后,则
返回SQLITE_BUSY错误码
通过两个锁来控制并发:
1. 通过pthread_mutex_lock进行线程锁,防止其他线程介入。然后比较状态
量,若当前状态不可跳转,则返回SQLITE_BUSY
2. 通过fcntl进行文件锁,防止其他进程介入。若锁失败,则返回SQLITE_BUSY
小结
多线程访问:
1. 开启SQLite多线程模式(Multi-thread)
默认不能保证同一个句柄在同一时间只有一个线程在操作,需要用户额外操作
2.开启WAL模式
确保了多线程读与读、读与写之间可以并发地进行
3.Busy Retry 方案
通过对堵塞线程进行 【休眠】【唤醒】【重试】流程优化
因此,尽管前面三个步骤一定程度满足多线程访问,但还是有一两个小问题
FMDB
基于 SQLite 的数据库框架
使用 Objective-C 语言对 SQLite 的 C 语言接口做了一层面向对象的封装
多线程安全:
过一个 Serial 队列保证在多线程环境下的数据安全
开启SQLite多线程模式,还要保证同一个句柄在同一时间只有一个线程在操作,
这么麻烦,干脆 串行得了简单了事
同SQLite 3 一样,串行队列效率比较低
FMDatabaseQueue持有 SQLite 句柄,多个线程使用同一个句柄,同时在初始
化时创建了一个串行队列,当在多线程之间执行数据库操作时,
FMDatabaseQueue将数据库操作以 block 的形式添加到该串行队列,然后按接
收顺序同步执行,以此来保证数据库在多线程下的数据安全
代码片段:
NSString *documentPath =
[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES) lastObject];
NSString *path = [documentPath
stringByAppendingPathComponent:@"demoDataBase.sqlite"];
_database = [FMDatabase databaseWithPath:path];
dispatch_queue_t queue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
[self.databaseQueue inDatabase:^(FMDatabase *db) {
BOOL result = [db executeUpdate:@"INSERT INTO Person (name,
sex) VALUES ('张三', '男')"];
if (result) {
NSLog(@"插入成功 - %@", [NSThread currentThread]);
}
}];
});
dispatch_async(queue, ^{
[self.databaseQueue inDatabase:^(FMDatabase *db) {
BOOL result = [db executeUpdate:@"INSERT INTO Person (name,
sex) VALUES ('李四', '男')"];
if (result) {
NSLog(@"插入成功 - %@", [NSThread currentThread]);
}
}];
});
});
ModelSQLiteKit
基于 SQLite 封装的 ORM 数据库操作开源库
支持直接将 Model 存入数据库,无需开发人员手动拼接 SQL 语句
封装了所有的常见数据库操作
在进行数据库操作时通过控制信号量来保证线程安全
线程的数据库操作按顺序同步进行
示例:
创建了一个值为1的信号量:
self.dsema = dispatch_semaphore_create(1);
数据库操作时通过信号量控制并发量:
+ (NSArray *)queryModel:(Class)model_class conditions:(NSArray
*)conditions queryType:(WHC_QueryType)query_type {
dispatch_semaphore_wait([self shareInstance].dsema,
DISPATCH_TIME_FOREVER);
NSArray *model_array = [self startQuery:model_class
conditions:conditions queryType:query_type];
dispatch_semaphore_signal([self shareInstance].dsema);
return model_array;
}
dispatch_async(dispatch_get_global_queue(0, 0), ^{
Person *person = [Person new];
person.name = @"张三";
person.age = 25;
[WHCSqlite insert:person];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
Person *person = [Person new];
person.name = @"李四";
person.age = 28;
[WHCSqlite insert:person];
});
WCDB
微信团队推出跨平台的数据库框架
基于 SQLCipher(SQLite的加密扩展)
WCDB 通过 SQLite 多句柄 和 WAL 日志模式 来支持线程间读与读、读与写操作
并发执行,并通过优化 Busy Retry 方案 来提升线程间写与写操作串行执行的效
率
WCDB 通过设置PRAGMA SQLITE_THREADSAFE=2将 SQLite 的线程模式设置
为【多线程(Multi-thread)模式】
开启SQLite多线程模式还不够,要保证同一个句柄在同一时间只有一个线程在操
作
HandlePool
WCDB 内置一个句柄池HandlePool,由它管理和分发 SQLite 句柄。WCDB 提供
的WCTDatabase、WCTTable和WCTTransaction的所有 SQL 操作接口都是线
程安全,它们不直接持有数据库句柄,而是由HandlePool根据数据库访问所在的
线程、是否处于事务、并发状态等,自动分发合适的 SQLite 连接进行操作,以
此来保证同一个句柄在同一时间只有一个线程在操作,从而达到读与读、读与写
并发的效果。
开启WAL模式
WCDB开启了 SQLite 的 WAL模式(Write-Ahead-Log),来进一步提升多线程
的并发性。
【WAL模式下】读写操作都是在日志文件上进行,写操作会先append到日志文
件末尾,而不是直接覆盖旧数据。而读操作开始时,会记下当前的日志文件状
态,并且只访问在此之前的数据。这就确保了多线程读与读、读与写之间可以并
发地进行。
【DELETE模式】下因为读写操作都是直接在db-page上面进行,因此读写操作
【必须串行】执行
小结
前面通过两步进行多线程访问:
1. 开启SQLite多线程模式(Multi-thread)
同时WCDB 内置一个句柄池HandlePool,由它管理和分发 SQLite 句柄,保证同
一个句柄在同一时间只有一个线程在操作
2.开启WAL模式
确保了多线程读与读、读与写之间可以并发地进行
问题?
但是,阻塞的情况也还是会发生。后来者还是必须在源码层等待之前的写操作完
成后才能继续
下面接着看看 Busy Retry的方案
优化Busy Retry方案
SQLite 的 Busy Retry 方案还有优化空间。
回顾一下:
Busy Retry
在Retry过程中,休眠时间的长短和重试次数,是决定性能和操作成功率的关键。
若休眠时间太短或重试次数太多,会空耗CPU的资源;若休眠时间过长,会造成
等待的时间太长;若重试次数太少,则会降低操作的成功率。
通过两个锁来控制并发:
1. 通过pthread_mutex_lock进行线程锁,防止其他线程介入。然后比较状态
量,若当前状态不可跳转,则返回SQLITE_BUSY
2. 通过fcntl进行文件锁,防止其他进程介入。若锁失败,则返回SQLITE_BUSY
(1 )SQLite 的 Busy Retry 方案:
(2)优化SQLite 的 Busy Retry 方案:
1. 通过pthread_mutex_lock进行线程锁,防止其他线程介入。然后比较状态
量,若当前状态不可跳转,则将当前期望跳转的状态,插入到一个FIFO的Queue
尾部。最后,线程通过pthread_cond_wait进入 休眠状态,等待其他线程的唤
醒。
2. 忽略文件锁
当解锁操作结束后:
取出Queue头部的状态量,并比较状态是否能够跳转。若能够跳转,则通过
pthread_cond_signal_thread_np唤醒对应的线程重试。
示例:
创建WCTDatabase:
NSString *documentPath =
[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES) lastObject];
NSString *path = [documentPath
stringByAppendingPathComponent:@"demoDataBase.sqlite"];
_database = [[WCTDatabase alloc] initWithPath:path];
多线程操作数据库:
dispatch_queue_t queue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,
0);
dispatch_async(queue, ^{
NSArray *messages = [_database
getAllObjectsOfClass:Message.class fromTable:@"message"];
/// ...
});
dispatch_async(queue, ^{
[_database insertObjects:messages into:@"message"];
});
最后
WCDB在多线程并发方面主要采取了以上方案,除了多线程方面的优化,WCDB
还做了如mmap优化、禁用内存统计锁、保留WAL文件大小等优化来进一步提高
SQLite的性能。
总结
FMDB 和 ModelSqliteKit 都是等同于串行队列任务,多线程安全但是牺牲效率
WCDB 却充分利用并优化 SQLite 能力,并高效多线程读写
要熟悉SQLite源码才能更好的优化