数据库多线程 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源码才能更好的优化