1、前言

众所周知,微信在后台服务器不保存聊天记录,微信在移动客户端所有的聊天记录都存储在一个 SQLite 数据库中,一旦这个数据库损坏,将会丢失用户多年的聊天记录。而我们监控到现网的损坏率是0.02%,也就是每 1w 个用户就有 2 个会遇到数据库损坏。考虑到微信这么庞大的用户基数,这个损坏率就很严重了。更严重的是我们用的官方修复算法,修复成功率只有 30%。损坏率高,修复率低,这两个问题都需要我们着手解决。

2、SQLite 损坏原因及其优化

我们首先来看 SQLite 损坏的原因,SQLite官网(http://www.sqlite.org/howtocorrupt.html)上列出以下几点

  • 文件错写
  • 文件锁 bug
  • 文件 sync 失败
  • 设备损坏
  • 内存覆盖
  • 操作系统 bug
  • SQLite bug

但是我们通过收集到的大量案例和日志,分析出实际上移动端数据库损坏的真正原因其实就3个:

  • 空间不足
  • 设备断电
  • 文件 sync 失败

我们需要针对这些原因一一进行优化。

2.1、优化空间占用

首先我们来优化微信的空间占用问题。在这之前微信的部分业务也做了空间清理,例如朋友圈会自动删除7天前缓存的图片。但是总的来说对文件空间的使用缺乏一个全局把控,全靠各个业务自觉。我们需要做得更积极主动,要让开发人员意识到用户的存储空间是宝贵的。我们采取以下措施:

  • 业务文件先申请后使用,如果某个文件没有申请就使用了,会被自动扫描出来并删除;
  • 每个业务文件都要申明有效期,是一天、一个星期、一个月还是永久存储;
  • 过期文件会被自动清理。

对于微信之外的空间占用,例如相册、视频、其他App的空间占用,微信本身是做不了什么事情的,我们可以提示用户进行空间清理:

2.2、优化文件 sync

2.2.1、synchronous = FULL

设置SQLite的文件同步机制为全同步,亦即要求每个事物的写操作是真的flush到文件里去。

2.2.1、fullfsync = 1

通过与苹果工程师的交流,我们发现在 iOS 平台下还有 fullfsync (https://www.sqlite.org/pragma.html#pragma_fullfsync) 这个选项,可以严格保证写入顺序跟提交顺序一致。设备开发商为了测评数据好看,往往会对提交的数据进行重排,再统一写入,亦即写入顺序跟App提交的顺序不一致。在某些情况下,例如断电,就可能导致写入文件不一致的情况,导致文件损坏。

2.3、优化效果

多管齐下之后,我们成功将损坏率降低了一半多;DB损坏还是无法完全避免,我们还是得提高修复成功率。

3、SQLite 修复逻辑优化

3.1、master 表

首先我们来看 SQLite 的架构。SQLite 使用 B+树 存储一个表,整个 SQLite 数据库就是这些 B+树 组成的森林。对于每个表的元数据(表名、根节点地址、表 scheme 等),都记录在一个叫 sql_master 的表中。这个 sql_master 表(下简称 master 表) 本身也是一个 B+树 存储的普通表。

3.2、官方修复算法率低下原因

官方修复算法是这样一个流程:从 master 表中读出一个个表的信息,根据根节点地址和创表语句来 select 出表里的数据,能 select 多少是多少,然后插入到一个新 DB 中。要注意的是 master 表他本身也是一个 B+树 形式的普通表,DB 第0页就是他的根节点。那么只要 master 表某个节点损坏,这个节点下面记录的表就都恢复不了。更坏的情况是 DB 第0页损坏,那么整个 master 表都读不出来,就导致整个DB都恢复失败。这就是官方修复算法成功率这么低的原因,太依赖 master 表了。

3.3、备份 master 表

那么最自然的想法,自然是另外备份一份 master 表了,也不需要用B+树,直接用数组序列化存储就好。我们只需要每隔一段时间轮询 master 表,看看最近有没有增删 table,有的话就全量备份。

3.3.1、备份时机

这里有个担忧,就是普通数据表的插入会不会导致表的根节点发生变化,也就是说 master 表会不会频繁变化,如果变化很频繁的话,我们就不能简单地进行轮询方案了。通过分析源码,我们发现 SQLite 里面 B+树 算法的实现是 向下分裂 的,也就是说当一个叶子页满了需要分裂时,原来的叶子页会成为内部节点,然后新申请两个页作为他的叶子页。这就保证了根节点一旦定下来,是再也不会变动的。实际的代码调试也证实了我们这个推论。所以说 master 表只会在新创建表或者删除一个表时才会发生变化,我们完全可以采用定时轮询方案。

3.3.2、备份文件有效性

接下来的难题是既然 DB 可以损坏,那么这个备份文件也会损坏,怎么办呢?我们采用了 双备份 的机制。具体来说就是会有新旧两个备份文件,每个文件头都加上 CRC 校验;每次备份时,从两个备份文件中选出一个进行覆盖。具体怎么选呢?优先选损坏那个备份文件,如果两个都有效,那么就选相对较旧的。这就保证了即使本次写入导致文件损坏,还有另外一份备份可以用。这个做法跟 Realm 标榜的 MVCC(多版本并发控制)的做法有异曲同工之妙,相当于确认新写入的文件有效之后,才使用新写入的文件,否则还是继续用旧的有效的文件。

前面提到 DB 损坏的一个常见场景是空间不足,这种情况下还要分配文件空间给备份文件也是会失败的。为了解决这个问题,我们采取 预先分配空间 的做法,初始值是 32K,大约可存 750 个表的元信息,后续则按照32K的倍数进行增长。

3.4、优化效果

通过备份 master 表,我们成功将修复成功率提高了一倍多。

4、其他

通过这些优化,我们提高了微信聊天记录存储的可靠性。这些优化实践,会同之前在并发性能方面的优化实践(微信iOS SQLite源码优化实践),将会合并到微信即将开源的 WCDB(WeChat Database)组件中。我们正在进行紧张的代码整理工作,争取在 2017 年年中开源 WCDB。


更多精彩内容欢迎关注腾讯 Bugly的微信公众账号:

腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的情况以及解决方案。智能合并功能帮助开发同学把每天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同学定位到出问题的代码行,实时上报可以在发布后快速的了解应用的质量情况,适配最新的 iOS, Android 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧!

微信 SQLite 数据库修复实践的更多相关文章

  1. 【教程】SQLite数据库修复

    SQLite 大家都知道,就不多说了. 有时候数据量大了,或者存储过程中出现异常,数据库就可能会出问题. 这是以前公司产品出现过的问题,导致软件都打不开了,我花了不少时间才解决的,趁现在有空贡献出来. ...

  2. 解密微信sqlite数据库

    最近在研究解密微信APP的数据库, 1.通过Android手机内置备份功能,可以获取到微信的数据库文件,再通过adb传到电脑上. 2.获取微信EnMicroMsg.db库的密码(通过IMEI和uin值 ...

  3. SQLite数据库损坏及其修复探究

    数据库如何发生损坏   SQLite 数据库具有很强的抗损坏能力.在执行事务时如果发生应用程序崩溃.操作系统崩溃甚至电源故障,那么在下次访问数据库文件时,会自动回滚部分写入的事务.恢复过程是全自动的, ...

  4. 修复 SQLite 数据库文件

    目 录 第1章 说明    1 1 下载SQLite Tools    1 2 运行    2 第1章 说明 笔者编写的一个程序,无法往 SQLite 数据库文件里写数据.使用SQLiteSpy打开该 ...

  5. 讨论SQLite数据库损坏与修复

      版权声明:博客将逐步迁移到 http://cwqqq.com https://blog.csdn.net/cwqcwk1/article/details/45541409 昨晚,朋友和我反馈SQL ...

  6. android: SQLite 数据库的最佳实践

    6.5.1    使用事务 前面我们已经知道,SQLite 数据库是支持事务的,事务的特性可以保证让某一系列的操 作要么全部完成,要么一个都不会完成.那么在什么情况下才需要使用事务呢?想象以下场 景, ...

  7. Sqlite 数据库出现database disk image is malformed报错的解决方法

    软件用的是Sqlite数据库,昨天还好好的,今天开机登录软件报错:database disk image is malformed 用Sqlite Expert Personal 重建索引,发现其中一 ...

  8. Android开发-之SQLite数据库

    之前我们讲了如何将数据存储在文件中,那么除了这种方式呢,就是我们常见的大家都知道的将数据存储在数据库当中了. 将数据存储在数据库中的优势: 1)存储在数据库中的数据更加方便操作,比如增.删.改.查等 ...

  9. 数据库优化实践【MS SQL优化开篇】

    数据库定义: 数据库是依照某种数据模型组织起来并存在二级存储器中的数据集合,此集合具有尽可能不重复,以最优方式为特定组织提供多种应用服务,其数据结构独立于应用程序,对数据的CRUD操作进行统一管理和控 ...

随机推荐

  1. Cisco交换机设置备份

    conf tusername xa privilege 3 secret xxx aaa new-modelaaa authentication login default local enablea ...

  2. oracle数据入库

    oracle数据入库 注意:先要处理文件中的分隔符   将数据分列 创建为标准的sql语句   1.在oracle数据库中创建要入库的表如果有该表则不用创建(注:创建字段的数据类型要符合实际逻辑 va ...

  3. VBA读写ini 配置文件

    Option Explicit 'read Private Declare Function GetPrivateProfileString Lib "kernel32" Alia ...

  4. Linux驱动之USB鼠标驱动编写

    本篇博客分以下几部分讲解 1.介绍USB四大描述 2.介绍USB鼠标驱动程序功能及框架 3.介绍程序用到的结构体 4.介绍程序用到的函数 5.编写程序 6.测试程序 1.介绍USB四大描述符 USB设 ...

  5. 分析easyswoole3.0源码,服务启动为例(一)

    swoole已经升级到4了,主要的特性是更好的支持协程,easyswoole也为了更好支持协程推出了es3.我本地环境是php7.2.9 centos7 在github下载最新的3.0的demo.地址 ...

  6. Js有一个已经排好序的数组。现输入一个数,要求按原来的规律将它插入数组中。

    <!DOCTYPE html> <title>Title</title> <script> var arr = [1,2,3,4,11]; var s ...

  7. python第十九天 关于方法,函数

    1.先从简单的函数说起 from inspect import isfunction 导入判断是否是function def foo():pass 定义了一个函数 print(foo) <fun ...

  8. Python unittest使用小结

    unittest是Python自带的单元测试框架,其中最核心的四个概念是:test case, test suite, test runner, test fixture. 流程:TestLoader ...

  9. 使用Python完成排序(快排法、归并法)

    class Sort(object): def quick_sort(self, ls): self.quick_sort_helper(ls, 0, len(ls) - 1) return ls d ...

  10. 返回上一页 html A标记代码

    <a class="sjad" href="#" onClick="javascript:history.back(-1);"> ...