文档章节

微信 SQLite 数据库修复实践

腾讯Bugly
 腾讯Bugly
发布于 2017/05/08 11:34
字数 2018
阅读 83
收藏 0

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 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧!

© 著作权归作者所有

共有 人打赏支持
腾讯Bugly
粉丝 279
博文 134
码字总数 568948
作品 0
深圳
C#数据本地存储方案之SQLite

即使是做网络应用,在断线情况下,也需要考虑数据的本地存储。在SQLite出现之前,数据量大的情况下,我们一直使用ACCESS,数据量小,则文件存储。ACCESS不支持事务原子性,在断电情况下(这种...

luminji
2010/12/19
0
0
[转载]SQLite 创建、修改、删除表

、数据库定义语言 DDL 在关系型数据库中,数据库中的表 Table、视图 View、索引 Index、关系 Relationship 和触发器 Trigger 等等,构成了数据库的架构 Schema。 在 SQL 语句中,专门有一些语...

mutouzhang
2014/03/25
0
0
【防衰老教程】-windows环境下安装SQLite

什么是 SQLite? SQLite是一个进程内的库,实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。它是一个零配置的数据库,这意味着与其他数据库一样,您不需要在系统中配置。...

Weijue
05/09
0
0
【原创】如何在 libgda 和 SQLite 之间作出选择

How to choose between Libgda and SQLite 如何在 libgda 和 SQLite 之间作出选择 (本文为官方文档翻译,以中英对照形式呈现) Libgda has been added as a new external dependency for G...

摩云飞
2013/03/20
0
0
linux下源码编译sqlite3数据库

尽管大多数linux下有sqlite3数据库,但是嵌入式开发的时候还是需要sqlite3数据库,下面为源码编译过程 1,下载SQLite源代码sqlite-3.6.23.1.tar.gz 2,复制sqlite-3.6.23.1.tar.gz到linux上的/us...

一一无念
2012/05/15
0
0

没有更多内容

加载失败,请刷新页面

加载更多

day92-20180918-英语流利阅读-待学习

健身最大的敌人不是懒惰,而是逞强 Daniel 2018-09-19 1.今日导读 还记得 2008 年北京奥运会运动员刘翔的退赛风波吗?那天几乎所有中国人都将视线聚焦在了鸟巢体育馆 110 米栏的项目上,迫不...

飞鱼说编程
28分钟前
2
0
70.shell的函数 数组 告警系统需求分析

20.16/20.17 shell中的函数 20.18 shell中的数组 20.19 告警系统需求分析 20.16/20.17 shell中的函数: ~1. 函数就是把一段代码整理到了一个小单元中,并给这个小单元起一个名字,当用到这段...

王鑫linux
今天
3
0
分布式框架spring-session实现session一致性使用问题

前言:项目中使用到spring-session来缓存用户信息,保证服务之间session一致性,但是获取session信息为什么不能再服务层获取? 一、spring-session实现session一致性方式 用户每一次请求都会...

WALK_MAN
今天
6
0
C++ yield()与sleep_for()

C++11 标准库提供了yield()和sleep_for()两个方法。 (1)std::this_thread::yield(): 线程调用该方法时,主动让出CPU,并且不参与CPU的本次调度,从而让其他线程有机会运行。在后续的调度周...

yepanl
今天
4
0
Java并发编程实战(chapter_3)(线程池ThreadPoolExecutor源码分析)

这个系列一直没再写,很多原因,中间经历了换工作,熟悉项目,熟悉新团队等等一系列的事情。并发课题对于Java来说是一个又重要又难的一大块,除非气定神闲、精力满满,否则我本身是不敢随便写...

心中的理想乡
今天
57
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部