SQLite 多线程模型实测分析

网上有很多多线程模型的分析,但是讲的都不是很清楚,大多都是直接下结论,并没有数据支撑。所以想着彻底了解下各种多线程模型,并且实际地测一测数据。

线程模式

在讨论线程模式的前,先弄清楚几个SQLITE_API:

  • sqlite3_open:返回一个数据库连接(句柄)。
  • sqlite3_prepare_v2 / sqlite3_bind_* / sqlite3_step:类似 sqlite3_exec,不过支持参数化 SQL,可以重复执行多次。
  • sqlite3_exec:执行 SQL 语句,等价于上面一组 API,将编译,执行进行了封装。

SQLite supports three different threading modes:

  1. Single-thread. In this mode, all mutexes are disabled and SQLite is unsafe to use in more than a single thread at once.
  2. 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.
  3. 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 种模型:

  1. 单线程模式 + 单句柄 + 串行队列
  2. 多线程模式 + 多句柄
  3. 串行模式 + 单句柄

别的模型就不用考虑了,因为:

  1. 单线程模式下要么崩溃,要么频繁报错,必须用串行队列。既然有了串行队列,也就没必要多句柄来增加建立句柄的时间。
  2. 多线程模式要是用串行队列,那就和上面的模型没有区别了,更因为多线程底层多了几处地方加锁,性能还会有点损耗。而单句柄是会崩溃的,所以只能选择多句柄。
  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)把多个写操作包在一起,一次性写入磁盘以提高性能。

疑问 对于事务优化,原先觉得是针对的批量写,对于只读按理说性能不会有提高,但是测下来只读的性能也大幅度增加,不明白为什么。

结论

虽然表结构很简单,也只是对单表操作,但是还是能说明一些问题:

  1. 多线程 + 多句柄 对于只读操作性能非常高。
  2. 单线程模式 + 单句柄 + 串行队列 要是加上事务,性能可以达到最优。

第三方库

FMDB

FMDB 没有指定线程模式,所以默认是多线程模式。

FMDatabasePool
类似多线程 + 多句柄模型,用了 Pool 防止句柄建的过多。不过官方说如果不是只读的情况,会导致死锁,我觉得并不会导致死锁,只是会频繁地报 DataBase Lock

FMDatabaseQueue
类似单线程模式 + 单句柄 + 串行队列模型,虽然是多线程模式,但其实性质是一样的,改成单线程模式性能应该还能增加一点点

GYDataCenter

最后安利一个GYDataCenter:高性能数据库框架,这是微信读书出的,在 FMDB 的基础上封装了一层,增加了面向对象的操作接口和 cache 层,而且增加了自动批量写入磁盘的功能,性能得到了大幅度增高。

作者:levi
comments powered by Disqus