网上有很多多线程模型的分析,但是讲的都不是很清楚,大多都是直接下结论,并没有数据支撑。所以想着彻底了解下各种多线程模型,并且实际地测一测数据。
线程模式
在讨论线程模式的前,先弄清楚几个SQLITE_API:
- sqlite3_open:返回一个数据库连接(句柄)。
- sqlite3_prepare_v2 / sqlite3_bind_* / sqlite3_step:类似 sqlite3_exec,不过支持参数化 SQL,可以重复执行多次。
- sqlite3_exec:执行 SQL 语句,等价于上面一组 API,将编译,执行进行了封装。
SQLite supports three different threading modes:
- Single-thread. In this mode, all mutexes are disabled and SQLite is unsafe to use in more than a single thread at once.
- Multi-thread. In this mode, SQLite can be safely used by multiple threads provided that no single database connection is used simultaneously in two or more threads.
- Serialized. In serialized mode, SQLite can be safely used by multiple threads with no restriction.
我测下来的结果:( 同时读写 )
模式 | 多线程单句柄 | 多线程多句柄 |
---|---|---|
串行 | 没问题 | sqlite3_step 会报 SQLITE_BUSY |
单线程 | sqlite3_prepare_v2 处崩溃 | sqlite3_open 会报 SQLITE_ERROR,sqlite3_step 会报 SQLITE_BUSY |
多线程 | sqlite3_prepare_v2 处崩溃 | sqlite3_step 会报 SQLITE_BUSY |
多线程多句柄的环境下,报 SQLITE_BUSY 不可避免。
多线程模型
由上面的测试可知,多线程环境下一共可以定 3 种模型:
- 单线程模式 + 单句柄 + 串行队列
- 多线程模式 + 多句柄
- 串行模式 + 单句柄
别的模型就不用考虑了,因为:
- 单线程模式下要么崩溃,要么频繁报错,必须用串行队列。既然有了串行队列,也就没必要多句柄来增加建立句柄的时间。
- 多线程模式要是用串行队列,那就和上面的模型没有区别了,更因为多线程底层多了几处地方加锁,性能还会有点损耗。而单句柄是会崩溃的,所以只能选择多句柄。
- 串行模式选择多句柄反而会 SQLITE_BUSY 浪费时间,所以选择单句柄。
typedef NS_ENUM(NSInteger, LYDatabaseType) {
LYDatabaseTypeThreadSingle = 1,
LYDatabaseTypeThreadMulti,
LYDatabaseTypeThreadSerialized,
};
static int lastReadCount = 0;
static int readCount = 0;
static int lastWriteCount = 0;
static int writeCount = 0;
static const NSInteger kMaxDataCount = 50000;
- (void)viewDidLoad {
[super viewDidLoad];
//统计每秒的读写次数
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(count) userInfo:nil repeats:YES];
}
- (void)count {
int lastRead = lastReadCount;
int lastWrite = lastWriteCount;
lastReadCount = readCount;
lastWriteCount = writeCount;
NSLog(@"---%d, %d---", lastReadCount - lastRead, lastWriteCount - lastWrite);
}
//单线程模式 + 单句柄 + 串行队列
- (void)singleTest:(NSString *) path {
LYDatabase *db = [LYDatabase databaseWithPath:path type:LYDatabaseTypeThreadSingle];
dispatch_queue_t conQueue = dispatch_queue_create("ly.con", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t serQueue = dispatch_queue_create("ly.ser", NULL);
if ([db open]) {
for (NSInteger i = 0; i < 10; i++) { //各开10个线程
dispatch_async(conQueue, ^{
while (1) {
dispatch_sync(serQueue, ^{
//只读测试
// BOOL readSuccess = [db executeUpdate:@"SELECT value FROM test WHERE id = ?" withArgumentsInArray:@[@(arc4random() % kMaxDataCount + 1)]];
// if (readSuccess) {
// readCount++;
// }
//只写测试
BOOL writeSuccess = [db executeUpdate:@"UPDATE test SET value = ? WHERE id = ?;" withArgumentsInArray:@[@(arc4random()), @(arc4random() % kMaxDataCount + 1)]];
if (writeSuccess) {
writeCount++;
}
});
}
});
}
}
}
//多线程模式 + 多句柄
- (void)contest:(NSString *) path {
dispatch_queue_t conQueue = dispatch_queue_create("ly.con", DISPATCH_QUEUE_CONCURRENT);
for (NSInteger i = 0; i < 10; i++) {
dispatch_async(conQueue, ^{
LYDatabase *db = [LYDatabase databaseWithPath:path type:LYDatabaseTypeThreadMulti];
if ([db open]) {
while (1) {
//只读测试
// BOOL readSuccess = [db executeUpdate:@"SELECT value FROM test WHERE id = ?" withArgumentsInArray:@[@(arc4random() % kMaxDataCount + 1)]];
// if (readSuccess) {
// readCount++;
// }
//只写测试
BOOL writeSuccess = [db executeUpdate:@"UPDATE test SET value = ? WHERE id = ?;" withArgumentsInArray:@[@(arc4random()), @(arc4random() % kMaxDataCount + 1)]];
if (writeSuccess) {
writeCount++;
}
}
}
});
}
}
//串行模式 + 单句柄
- (void)serTest:(NSString *) path {
dispatch_queue_t conQueue = dispatch_queue_create("ly.con", DISPATCH_QUEUE_CONCURRENT);
LYDatabase *db = [LYDatabase databaseWithPath:path type:LYDatabaseTypeThreadSerialized];
if ([db open]) {
for (NSInteger i = 0; i < 10; i++) {
dispatch_async(conQueue, ^{
while (1) {
//只读测试
// BOOL readSuccess = [db executeUpdate:@"SELECT value FROM test WHERE id = ?" withArgumentsInArray:@[@(arc4random() % kMaxDataCount + 1)]];
// if (readSuccess) {
// readCount++;
// }
//只写测试
BOOL writeSuccess = [db executeUpdate:@"UPDATE test SET value = ? WHERE id = ?;" withArgumentsInArray:@[@(arc4random()), @(arc4random() % kMaxDataCount + 1)]];
if (writeSuccess) {
writeCount++;
}
}
});
}
}
}
复制代码
实测数据
实验环境: 2.7 GHz Intel Core i5 / 8 G 内存 / 模拟器iPhoneX db 里的有 test 表,表里有 50000 条数据,每次都开 10 个线程。 如果最终值能稳定下来,取最终稳定的平均值。
不开WAL
模型 | 只读 | 只写 | 读写 |
---|---|---|---|
单线程模式 + 单句柄 + 串行队列 | 18000 | 16000 | 8500 / 8500 |
多线程模式 + 多句柄 | 43000 | 9500 | 25000 / 50-800(不稳定) |
串行模式 + 单句柄 | 15500 | 13300 | 7800 / 6300 |
打开WAL
模型 | 只读 | 只写 | 读写 |
---|---|---|---|
单线程模式 + 单句柄 + 串行队列 | 36000 | 35000 | 18000 / 18000 |
多线程模式 + 多句柄 | 72000 | 20000-35000(不稳定) | 17000-35000(不稳定) / 2500-3200(不稳定) |
串行模式 + 单句柄 | 27000 | 22500 | 13800 / 11000 |
//加上事务,每秒 commit 一次
[db executeStatements:@"begin exclusive transaction"];
[NSTimer scheduledTimerWithTimeInterval:1 * NSEC_PER_SEC repeats:YES block:^(NSTimer * _Nonnull timer) {
[db executeStatements:@"commit transaction"];
[db executeStatements:@"begin exclusive transaction"];
}];
复制代码
这种行为只对 单线程模式 + 单句柄 + 串行队列 这种模型有巨大的改良:
模型 | 只读 | 只写 | 读写 |
---|---|---|---|
单线程模式 + 单句柄 + 串行队列 | 70000 | 76000 | 35000 / 35000 |
要是在使用事务的基础上,再打开 WAL,性能还会增加一点,但不是很明显。
想法
WAL 机制的原理是:修改并不直接写入到数据库文件中,而是写入到另外一个称为 WAL 的文件中;如果事务失败,WAL 中的记录会被忽略,撤销修改;如果事务成功,它将在随后的某个时间被写回到数据库文件中,提交修改。 同步 WAL 文件和数据库文件的行为被称为 checkpoint(检查点),它由 SQLite 自动执行,默认是在 WAL 文件积累到 1000 页修改的时候;当然,在适当的时候,也可以手动执行 checkpoint,SQLite 提供了相关的接口。执行 checkpoint 之后,WAL 文件会被清空。 在读的时候,SQLite 将在 WAL 文件中搜索,找到最后一个写入点,记住它,并忽略在此之后的写入点(这保证了读写和读读可以并行执行);随后,它确定所要读的数据所在页是否在 WAL 文件中,如果在,则读 WAL 文件中的数据,如果不在,则直接读数据库文件中的数据。 在写的时候,SQLite 将之写入到 WAL 文件中即可,但是必须保证独占写入,因此写写之间不能并行执行
所以很明显,WAL 机制对于读写都有性能提高。
当数据库连续执行多个写操作时,可以通过事务(transaction)把多个写操作包在一起,一次性写入磁盘以提高性能。
疑问 对于事务优化,原先觉得是针对的批量写,对于只读按理说性能不会有提高,但是测下来只读的性能也大幅度增加,不明白为什么。
结论
虽然表结构很简单,也只是对单表操作,但是还是能说明一些问题:
- 多线程 + 多句柄 对于只读操作性能非常高。
- 单线程模式 + 单句柄 + 串行队列 要是加上事务,性能可以达到最优。
第三方库
FMDB
FMDB 没有指定线程模式,所以默认是多线程模式。
FMDatabasePool
类似多线程 + 多句柄模型,用了 Pool 防止句柄建的过多。不过官方说如果不是只读的情况,会导致死锁,我觉得并不会导致死锁,只是会频繁地报 DataBase Lock。
FMDatabaseQueue
类似单线程模式 + 单句柄 + 串行队列模型,虽然是多线程模式,但其实性质是一样的,改成单线程模式性能应该还能增加一点点。
GYDataCenter
最后安利一个GYDataCenter:高性能数据库框架,这是微信读书出的,在 FMDB 的基础上封装了一层,增加了面向对象的操作接口和 cache 层,而且增加了自动批量写入磁盘的功能,性能得到了大幅度增高。