sqliteDEV Club 分享笔记(微信iOS SQLite源码优化实践)

<strong>首先,要谢谢 DEV Club
能给自身一个那样好的平台,在这边学到大牛们分享的技术.这一次感谢的是张三华,张哥的这样好的分享.从中学到很多,在这做个摘抄笔记.</strong>
   <strong>下面起初我们今日的分享。SQLite是我们在移动端常用的数据库,微信也是遵照它包裹了一层ObjC接口。大家知晓,微信里音信的收发是很频繁的,尤其是对于重度用户,这对于数据库的多线程并发和I/O是很大的挑衅。平常对这有些做优化,有二种艺术,一是修改SQLite的参数,如Cache
Size等,二是改业务层调用,如主线程操作dispatch到子线程。然则,前者有众所周知的瓶颈,后者则是个endless的做事。我们盼望能一劳永逸地缓解同类题目。这就是大家此次所要分享的优化。</strong>

一.大家先讲SQLite所提供的多线程并发方案。它对这上头的支撑做的很不利,在采纳上,只需:

  1. 翻开句柄多线程帮助的部署 PRAGMA SQLITE_THREADSAFE=2

  2. 管教同一个句柄同一时间只有一个线程在操作

  3. (可选)开启WAL模式PRAGMA journal_mode=WAL

    这时候写操作会先append到wal文件末尾,而不是直接覆盖旧数据。而读操作起来时,会记录当前的WAL文件状态,并且只访问此前的数码。这就确保了多线程读与读、读与写之间可以并发地展开.
    而写与写之间仍会互相阻塞。SQLite提供了Busy
    Retry的方案,即发生阻塞时,会触发Busy
    Handler,此时得以让线程休眠一段时间后,重新尝试操作。重试一定次数如故败北后,则赶回SQLITE_BUSY错误码。

    下边这段代码是SQLite默认的Busy Handler

SQLite 默认的 Busy Handler.jpeg

   上边介绍了SQLite多线程并发方案,接下去大家把关键放在Busy
Retry这多少个方案的欠缺上. Busy
Retry的方案就算基本能化解问题,但对性能的压榨做的不够极致。在Retry过程中,休眠时间的长度和重试次数,是决定性能和操作成功率的首要。
然则,它们的最优值,因不同操作不同场景而不同。若休眠时间太短或重试次数太多,会空耗CPU的资源;若休眠时间过长,会导致等待的时刻太长;若重试次数太少,则会减低操作的成功率。如下图

等待.重复.jpeg

可以看出

  • CPU空转这段,线程一操作还没竣工,这里空耗了CPU的资源
  • 线程闲置这段,线程一曾经停止,而线程二仍在伺机,空耗了时光
    对于这么些的优化,简单的办法可以是修改休眠时间,尽最大限度裁减以上两段空耗的资源。
    我们通过A/B Test对两样休眠时间举办了试验,拿到了如下的结果

A:B Test.jpeg

   可以看看,倘诺休眠时间与重试成功率的关系,遵照肉色的曲线举行分布,那么p点的值也不失为该方案的一个次优解。然则不同工作和操作的需求,仍旧有很大的不等的。
既然SQLite的方案充裕,大家将要先河往深层探索新的可能了。

(1).上边将介绍SQLite中控制并发相关的规律

SQLite是一个适配不同平台的数据库,不仅辅助多线程并发,还匡助多进程并发。它的主导逻辑可以分成两部分:

SQLite 主题逻辑.jpeg

  • Core层。包括了接口层、编译器和虚拟机。通过接口传入SQL语句,由编译器编译SQL生成虚拟机的操作码opcode。而虚拟机是基于生成的操作码,控制Backend的一言一行。
  • Backend层。由B-Tree、Pager、OS三片段构成,实现了数据库的存取数据的基本点逻辑。
        在架设最底端的OS层是对两样操作系统的系列调用的抽象层。它实现了一个VFS(Virtual
    File
    System),将OS层的接口在编译时映射到对应操作系统的系列调用。锁的贯彻也是在这边展开的。
    SQLite通过六个锁来支配并发。首个锁对应DB文件,通过5种状态进行保管;第二个锁对应WAL文件,通过改动一个16-bit的unsigned
    short
    int的每一个bit举行管制。虽然锁的逻辑有局部繁杂,但这里并不需关注。这三种锁最后都落在OS层的sqlite3OsLock、sqlite3OsUnlock和sqlite3OsShmLock上具体实现。它们在锁的贯彻相比较接近。以lock操作在iOS上的落实为例:
  1. 通过pthread_mutex_lock举办线程锁,制止其他线程参加。然后相比状态量,若当前情形不行跳转,则赶回SQLITE_BUSY

  2. 透过fcntl举行文件锁,制止其他进程参与。若锁退步,则赶回SQLITE_BUSY

    而SQLite采纳Busy Retry的方案的缘故也多亏在此

    文本锁没无线程锁类似pthread_cond_signal的通知机制。当一个历程的数据库操作截至时,不能通过锁来第一时间通告到其他进程展开重试。因此不得不退而求其次,通过反复休眠来拓展尝试。

(2).搞精通了SQLite并发的兑现,大家就是足以开首改造了。

我们通晓,iOS
app是单进程的,并从未多进程并发的要求,这和SQLite的规划初衷是不均等的。这就给大家的优化提供了辩论上的底子。在iOS这一一定情景下,我们可以放弃兼容性,提高并发性。
   新的方案修改为,当OS层举行lock操作时:

  1. 通过pthread_mutex_lock举办线程锁,避免其他线程插手。然后相比较状态量,若当前气象不行跳转,则将近年来期待跳转的状态,插入到一个FIFO的Queue尾部。最终,线程通过pthread_cond_wait进入
    休眠状态,等待其他线程的提拔。

    当OS层的unlock操作截至后:

  2. 取出Queue头部的状态量,并相比状态是不是可以跳转。若可以跳转,则透过pthread_cond_signal_thread_np唤醒对应的线程重试。

优化后的等候和重复.jpeg

  新的方案可以在DB空闲时的第一时间,公告到任何正在等待的线程,最大程度地下降了空等待的日子,且准确科学。其余,由于Queue的留存,当主线程被其他线程阻塞时,能够将主线程的操作“插队”到Queue的头部。当其他线程发起唤醒通告时,主线程能够有更高的优先级,从而降低用户可感知的卡顿.

二.下边介绍了多线程并发的优化,接下去将介绍I/O方面的优化。

涉嫌I/O效用的升官,最容易想到的就是mmap了,它可以减小多少从kernel层到user层的数量拷贝,从而提升效率。SQLite不仅扶助mmap,而且引进应用,在多数阳台是在早晚水准上默认打开的。
可是早期的iOS版本的存在部分bug,SQLite在编译层就倒闭了在iOS上对mmap的支撑,并且后知后觉地在16年九月才重新打开。所以如若利用的SQLite版本较低,还需注释掉相关代码后,重新编译生成后,才足以大快朵颐上mmap的性能。下图就是SQLite注释掉相关代码的commit

mmap 相关代码.jpeg

   开启mmap后,SQLite性能将享有升级,但这还不够。因为它只会对DB文件举行了mmap,而WAL文件分享不到这一个优化。原因如下:

  • 敞开WAL情势后,写入的数据会先append到WAL文件的结尾。待文件增长到自然长度后,SQLite会举行checkpoint。那一个长度默认为1000个页大小,在iOS上约为3.9MB。而在多句柄下,对WAL文件的操作是互相的。一旦某个句柄将WAL文件裁减了,而从未一个通报机制让任何句柄进行革新mmap的内容。此时其他句柄若使用mmap操作已被缩短的情节,就会招致crash。而平时的I/O接口,则只会回去错误,不会造成crash。因而,SQLite没有兑现对WAL文件的mmap。
    显然SQLite的统筹是指向容量较小的设施,尤其是在十几年前的可怜年代,这样的装置并不在少数。而随着硬盘价格逐年下降,对于像红米这样的配备,几MB的长空已经不复是急需斤斤计较的了。

  • 一派,文件再度增长,对于文件系统来说,这就代表需要耗费时间再次寻找合适的公文块.

衡量两者,大家得以改为

  1. 数据库关闭并checkpoint成功时,不再truncate或删除WAL文件,只修改WAL的文本头的Magic
    Number。下次数据库打开时,SQLite会识别到WAL文件不可用,重新从头起先写入。
  2. 为WAL添加mmap的支持

有了地点三个优化,全体性能就会提高广大了。

此处我从没贴具体代码需要改哪些地点,一方面是因为改变点较零散,另一方面是代码上的变更并不难。这些优化的工作量紧即便在SQLite原理和优化点的发掘上了,我们可以遵照优化方案去尝尝。可是我们还有部分简单易行且效果还不易的小优化,希望可以改为我们打开SQLite黑盒的一个关口。

率先个是剥夺文件锁。如我们在多线程优化时所说,对于iOS
app并不曾多进程的需要。因而大家可以直接注释掉os_unix.c中保有文件锁相关的操作。也许你会很想得到,固然尚未公文锁的需求,但这多少个操作耗时也很短,是否有必不可少专门优化呢?其实并不完全。耗时稍微是比出来。
SQLite中有cache机制。被加载进内存的page,使用完毕后不会即时释放。而是在早晚限制内经过LRU的算法更新page
cache。这就意味着,假设cache设置得当,大部分读操作不会读取新的page。可是因为文件锁的存在,本来只需在内存层面举行的读操作,不得不举办至少三遍I/O操作。而大家明白,I/O操作是远远慢于内存操作的。

第二个是禁用内存总括锁。SQLite会对报名的内存举行总计,而那多少个总结的数目都是置于同一个全局变量里开展测算的。这就代表总计前后,都是急需加线程锁,避免出现多线程问题的。以下SQLite内存申请的函数可以看看,当内存总计打开时,会跑代码的第二个if,malloc的上下被锁珍重了起来。

SQLite 内存申请函数.jpeg

   其实这里内存申请的量不大,并不是可怜耗时的操作,但却很频繁。多线程并发时,各线程很容易相互阻塞。因为耗时很短,所以被堵塞的时间也很短暂。似乎不会有太大问题。但往往地打断却表示线程不断地切换,这是个很影响属性的操作,尤其对于单核设备。由此,假诺不需要内存总计的特点,可以透过sqlite3_config(SQLITE_CONFIG_MEMSTATUS,
0)进行关闭。这个修改即使不需要转移源码,但如若不查看源码,恐怕是相比难发现的。

上述就是自己前些天的享受,总的来说,移动客户端数据库固然不如后台数据库那么复杂,但也设有着重重可挖掘的技术点。本次也只尝试了对SQLite原有的方案举行优化,而市面上还有好多可以的数据库,如LevelDB、RocksDB、Realm等,它们采纳了和SQLite不同的落实原理。后续大家将以此为戒它们的优化经验,尝试更长远的优化。

三,问答互动

1: 请问微信在全文索引上有实践吧?有没有自己做地方的搜索索引
答:SQLite是援助有全文索引的补助的,大家要做的是提供一个好的,匡助普通话的分词器。

2:请问微信在db文件修复上有什么经验呢?
答:看来我们对db文件损坏很关心啊。SQLite提供了PRAGMA
integrity_check的工具检测损坏
和DUMP工具导出损坏db。但从实践来看,效果并不地道。我们使用了按BTree结构遍历修复的法子,未来有机会可以大快朵颐给我们

3:请问有没有对能耗的监测和优化经验?
答:检测相关的咱们有卡顿监控序列,可以到大家的公众号WeMobileDev上询问@talisk-斗鱼电视机

4:iOS客户端用操作数据库需要每一回先open,执行完了再close,每一遍都这样,依然app只需要开关一遍相比好吧?
答:常用的db没有必要通常开关,db占用的内存并不高,可以衡量一下

5:微信对于地方空间不足会有一个强指示,这是出于咋样考虑?不同机型有两样的国策吗?
答:空间欠缺是个硬伤,所谓巧妇难为无米之炊。如16GB的vivo,其实很影响正常使用了。不同机型会做细化.

6:微信对于数据库升级有没有特意优化的地点?或者说不同版本的跳版本升级
答:不精晓这多少个题目值得是SQLite的升官或者表结构的升官。前者的话,暂时没见到SQLite新本子有相比较大的特征值得我们跟进。后者可以用alter
table在封装层匡助提升,性能损坏不大.

7:微信
sqllite数据库用的内存数据库吗?这和文件数据库导入导出怎么决定的?
答:没有使用内存数据库.

网站地图xml地图