1
线程安全和性能
线程安全
在讲线程安全和性能前,必须要了解 SQLite 是怎么实现线程安全和达到高性能,具体可以参考《SQLite 线程安全和并发》。使用 SQLite 时常规的优化方案无非是
- 缓存
sqlite3_prepare编译结果 - 使用
WAL模式 - 采用多线程模式,单写多读
- 合理安排事务
简单来说,WCDB 的连接池通过读写锁保证线程安全,和 FMDatabasePool 使用 gcd queue 并没有太多差异,一些肉眼可见的区别在于
WCDB并不对外暴露数据库连接对象,以减少外面错误使用的几率。WCDB在连接池之外还提供基于ThreadLocal的缓存机制,保证当前事务操作下永远只使用同一个连接。 (详见Database::flowOut)- 内部自动约束并发数,并对不合理的并发做出提示。比如连接数超过
std::thread::hardware_concurrency()就会有警告。 (详见HandlePool::flowOut) - 连接回收基于
C++变量作用域。这一点上在我看倒没有明显的优劣点,反倒有点炫技的成分,为了实现这一点还需要额外引入RecylceHandle。 - 支持内存不足时的数据库连接自动回收。 (详见
Database::purgeFreeHandles)
这些细微的差别能够使得 WCDB 在保证线程安全和合理并发的前提下,使用起来更加方便安心。
性能
除了上面说的合理设计框架,合理提供并发外,WCDB 还做了一些额外性能有点。下面仅列出一些我读代码和 wiki 的发现。
checkpointing优化
在使用 WAL 模式时,默认情况下,当 WAL 文件 大小超过 1000 个页大小时,SQLite 就会尝试将 WAL 文件 写回数据库文件,这就是所谓的 checkpointing。(详见 wal) 那么在大量数据批量写入的场景下,可能会不停的产生提交文件到数据库的事务。而 WCDB 的做法则是在触发 checkpointing 时,通过延时队列进行,避免大量写入时不停的触发 WalCheckpoint 调用。
代码如下
[](std::shared_ptr<Handle> &handle, Error &error) -> bool {
handle->registerCommittedHook(
[](Handle *handle, int pages, void *) {
static TimedQueue<std::string> s_timedQueue(2);
if (pages > 1000) {
s_timedQueue.reQueue(handle->path);
}
static std::thread s_checkpointThread([]() {
pthread_setname_np(
("WCDB-" + Database::defaultCheckpointConfigName)
.c_str());
while (true) {
s_timedQueue.waitUntilExpired(
[](const std::string &path) {
Database database(path);
WCDB::Error innerError;
database.exec(StatementPragma().pragma(
Pragma::WalCheckpoint),
innerError);
});
}
});
static std::once_flag s_flag;
std::call_once(s_flag,
[]() { s_checkpointThread.detach(); });
},
nullptr);
return true;
},
通过 TimedQueue 将同个数据库的 WalCheckpoint 合并延迟到 2 秒后统一进行。
SQLITE_BUSY优化
SQLite 的机制并不允许进行多线程同时进行写操作,当发生多个线程进行写操作时未得到锁的那一方将直接返回 SQLITE_BUSY。从 FMDB 的提交记录我们可以看出,ccgus 对怎么处理 SQLITE_BUSY 也是相当头疼,具体可以参考 FMDB 中关于 SQLITE_BUSY 的 issues。目前 FMDB 的做法是默认重试 2 秒,在此期间调用 sqlite3_sleep 随机休眠几十毫秒,等待另外一个线程释放锁。这种处理方式可以较大程度上缓解 SQLITE_BUSY 的问题,但仍不可避免。这也是 WCDB Benchmark 认为 FMDB 无法支持 Multi-Thread WriteWrite 的原因。
而 WCDB 的处理方式则相当粗暴:通过修改 sqlcipher 源码,如果当前未进入事务状态而产生 SQLITE_BUSY 则会挂起等待,超时时间为 10 秒。详细代码可以参见 btree.c 中的 sqlite3BtreeBeginTrans 方法。
do {
//一堆判断
sqlite3PagerBegin(pBt->pPager,wrflag>1,sqlite3TempInMemory(p->db));
//一堆判断
}while( (rc&0xFF)==SQLITE_BUSY && pBt->inTransaction==TRANS_NONE &&
btreeInvokeBusyHandler(pBt) );
- 编译选项优化
SQLite 有大量预编译宏选项可以配置,具体可以参见 sqliteLimit.h 和 sqliteInt.h,WCDB 也对此作了较多配置,具体可以参考 sqlchiper-preprocessed.xcodeproj 中的宏定义。像我在 《SQLite 分表》 提到的 SQLITE_MALLOC_SOFT_LIMIT 就是偷师自微信,通过设置它为 0,可以加快在大量表情况下的初始化过程。从微信分享给出的资料还有相当多的优化项,如 开启 mmap,禁用文件锁(针对 iOS单进程的场景)等,具体可以参考 《微信iOS SQLite源码优化实践》 并查找对应源码进行对照。