SQLite损坏修复

问题背景

目前后台服务器应该是不保存聊天记录,口袋助理iOS端的所有聊天记录都存储在一个 SQLite 数据库中,一旦这个数据库损坏,将会丢失用户的聊天记录。


解决思路

预防措施:

SQLite 是一个号称每行代码都有对应测试的成熟框架,其代码问题导致的 bug 非常少见。而一般损坏原因主要有3点:

  1. 空间不足
  2. 设备断电或 AppCrash
  3. 文件 sync 失败

针对空间不足:

通过中度的使用和观察,我发现 iOS 端的空间占用是相对合理的,并没有对存储空间的明显浪费。并且 App 会在数据库写入时检查可用空间,如果不足时会抛出空间不足的提示。

针对设备断电或App崩溃:

设备断电属于不可抗力。而 App 崩溃目前我们准备上线 APM 监控平台,预期在一到两个版本的迭代中把崩溃率降低到千分之一以下的行业优秀水平。

针对文件 sync 失败:

调整 synchronous = FULL , 保证每个事务的操作都能写入文件。目前CoreData的默认配置项。
调整 fullfsync = 1 , 保证写入文件顺序和提交顺序一致,拒绝设备重排顺序以优化性能。此项会降低性能。对比得出写入性能大概降低至默认值的25%左右。

优化效果:

根据微信的实践,调整配置项后,损坏率可以降低一半,但并不能完全避免损坏,所以我们还是需要补救措施。

补救措施:

通过查阅 SQLite 的相关资料,发现修复损坏数据库的两种思路和四种方案。

思路一:数据导出

.dump修复

从 master 表中读出一个个表的信息,根据根节点地址和创表语句来 select 出表里的数据,能 select 多少是多少,然后插入到一个新 DB 中。

每个SQLite DB都有一个sqlite_master表,里面保存着全部table和index的信息(table本身的信息,不包括里面的数据哦),遍历它就可以得到所有表的名称和 CREATE TABLE ...的SQL语句,输出CREATE TABLE语句,接着使用SELECT * FROM ... 通过表名遍历整个表,每读出一行就输出一个INSERT语句,遍历完后就把整个DB dump出来了。 这样的操作,和普通查表是一样的,遇到损坏一样会返回SQLITE_CORRUPT,我们忽略掉损坏错误, 继续遍历下个表,最终可以把所有没损坏的表以及损坏了的表的前半部分读取出来。将 dump 出来的SQL语句逐行执行,最终可以得到一个等效的新DB。

思路二:数据备份

拷贝:

不能再直白的方式。由于SQLite DB本身是文件(主DB + journal 或 WAL), 直接把文件复制就能达到备份的目的。

.dump备份:

上一个恢复方案用到的命令的本来目的。在DB完好的时候执行.dump, 把 DB所有内容输出为 SQL语句,达到备份目的,恢复的时候执行SQL即可。

Backup API:

SQLite自身提供的一套备份机制,按 Page 为单位复制到新 DB, 支持热备份。

综合思路:备份master表+数据导出

WCDB框架:

数据库完整时备份master表,数据库损坏时通过使用已备份的master表读取损坏数据库来恢复数据。成功率大概是70%。缺点在于我们目前项目使用的是CoreData框架,迁移成本非常的高。没有办法使用。

补救措施选型原则:

这么多的方案孰优孰劣?作为一个移动APP,我们追求的就是用户体验,根据资料推断只有万分之一不到的用户会发生DB损坏,不能为了极个别牺牲全体用户的体验。不影响用户体验的方法就是好方案。主要考量指标如下:

一:恢复成功率

由于牵涉到用户核心数据,“姑且一试”的方案是不够的,虽说 100% 成功率不太现实,但 90% 甚至 99% 以上的成功率才是我们想要的。

二:备份大小:

原本用户就可能有2GB 大的 DB,如果备份数据本身也有2GB 大小,用户想必不会接受。

三:备份性能:

性能则主要影响体验和备份成功率,作为用户不感知的功能,占用太多系统资源造成卡顿 是不行的,备份耗时越久,被系统杀死等意外事件发生的概率也越高。

数据导出方案考量:

恢复成功率大概是30%。不需要事先备份,故备份大小和备份性能都是最优的。

备份方案考量:

备份方案的理论恢复成功率都为100%,需要考量的即为备份大小和性能。

  • 拷贝:备份大小等于原文件大小。备份性能最好,直接拷贝文件,不需要运算。
  • Backup API: 备份大小等于原文件大小。备份性能最差,原因是热备份,需要用到锁机制。
  • .dump:因为重新进行了排序,备份大小小于原文件。备份性能居中,需要遍历数据库生成语句。

可以看出,比较折中的选择是 Dump ,备份大小具有明显优势,备份性能尚可,恢复性能较差但由于需要恢复的场景较少,算是可以接受的短板。

深入钻研

即使优化后的方案,对于大DB备份也是耗时耗电,对于移动APP来说,可能未必有这样的机会做这样重度的操作,或者频繁备份会导致卡顿和浪费使用空间。
备份思路的高成本迫使我们从另外的方案考虑,于是我们再次把注意力放在之前的Dump方案。 Dump 方案本质上是尝试从坏DB里读出信息,这个尝试一般来说会出现两种结果:

  1. DB的基本格式仍然健在,但个别数据损坏,读到损坏的地方SQLite返回SQLITE_CORRUPT错误, 但已读到的数据得以恢复。
  2. 基本格式丢失(文件头或sqlite_master损坏),获取有哪些表的时候就返回SQLITE_CORRUPT, 根本没法恢复。

第一种可以算是预期行为,毕竟没有损坏的数据能部分恢复。从成功率来看,不少用户遇到的是第二种情况,这种有没挽救的余地呢?

要回答这个问题,先得搞清楚sqlite_master是什么。它是一个每个SQLite DB都有的特殊的表, 无论是查看官方文档Database File Format,还是执行SQL语句 SELECT * FROM sqlite_master;,都可得知这个系统表保存以下信息: 表名、类型(table/index)、 创建此表/索引的SQL语句,以及表的RootPage。sqlite_master的表名、表结构都是固定的, 由文件格式定义,RootPage 固定为 page 1。

正常情况下,SQLite 引擎打开DB后首次使用,需要先遍历sqlite_master,并将里面保存的SQL语句再解析一遍, 保存在内存中供后续编译SQL语句时使用。假如sqlite_master损坏了无法解析,“Dump恢复”这种走正常SQLite 流程的方法,自然会卡在第一步了。为了让sqlite_master受损的DB也能打开,需要想办法绕过SQLite引擎的逻辑。

由于SQLite引擎初始化逻辑比较复杂,为了避免副作用,没有采用hack的方式复用其逻辑,而是决定仿造一个只可以 读取数据的最小化系统。

虽然仿造最小化系统可以跳过很多正确性校验,但sqlite_master里保存的信息对恢复来说也是十分重要的, 特别是RootPage,因为它是表对应的B-tree结构的根节点所在地,没有了它我们甚至不知道从哪里开始解析对应的表。

sqlite_master信息量比较小,而且只有改变了表结构的时候(例如执行了CREATE TABLE、ALTER TABLE 等语句)才会改变,因此对它进行备份成本是非常低的,一般手机典型只需要几毫秒到数十毫秒即可完成,一致性也容易保证, 只需要执行了上述语句的时候重新备份一次即可。有了备份,我们的逻辑可以在读取DB自带的sqlite_master失败的时候 使用备份的信息来代替。

到此,初始化必须的数据就保证了,可以仿造读取逻辑了。我们常规使用的读取DB的方法(包括dump方式恢复), 都是通过执行SQL语句实现的,这牵涉到SQLite系统最复杂的子系统——SQL执行引擎。我们的恢复任务只需要遍历B-tree所有节点, 读出数据即可完成,不需要复杂的查询逻辑,因此最复杂的SQL引擎可以省略。同时,因为我们的系统是只读的, 写入恢复数据到新 DB 只要直接调用 SQLite 接口即可,因而可以省略同样比较复杂的B-tree平衡、Journal和同步等逻辑。 最后恢复用的最小系统只需要:

  1. VFS读取部分的接口(Open/Read/Close),或者直接用stdio的fopen/fread、Posix的open/read也可以
  2. B-tree解析逻辑

Database File Format 详细描述了SQLite文件格式, 参照之实现B-tree解析可读取 SQLite DB。

实现了上面的逻辑,就能读出DB的数据进行恢复了,但还有一个小插曲。我们知道,使用SQLite查询一个表, 每一行的列数都是一致的,这是Schema层面保证的。但是在Schema的下面一层——B-tree层,没有这个保证。 B-tree的每一行(或者说每个entry、每个record)可以有不同的列数,一般来说,SQLite插入一行时, B-tree里面的列数和实际表的列数是一致的。但是当对一个表进行了ALTER TABLE ADD COLUMN操作, 整个表都增加了一列,但已经存在的B-tree行实际上没有做改动,还是维持原来的列数。 当SQLite查询到ALTER TABLE前的行,缺少的列会自动用默认值补全。恢复的时候,也需要做同样的判断和支持, 否则会出现缺列而无法插入到新的DB。

解析B-tree方案上线后,成功率约为78%。这个成功率计算方法为恢复成功的 Page 数除以总 Page 数。 由于是我们自己的系统,可以得知总 Page 数,使用恢复 Page 数比例的计算方法比人数更能反映真实情况。 B-tree解析好处是准备成本较低,不需要经常更新备份,对大部分表比较少的应用备份开销也小到几乎可以忽略, 成功恢复后能还原损坏时最新的数据,不受备份时限影响。 坏处是,和Dump一样,如果损坏到表的中间部分,比如非叶子节点,将导致后续数据无法读出。

落地实践:

剥离封装RepairKit:

从WCDB框架中,剥离修复组件,并且封装其C++的原始API为OC管理类。

备份 master 表的时机:

我们发现 SQLite 里面 B+树 算法的实现是 向下分裂 的,也就是说当一个叶子页满了需要分裂时,原来的叶子页会成为内部节点,然后新申请两个页作为他的叶子页。这就保证了根节点一旦下来,是再也不会变动的。master 表只会在新创建表或者删除一个表时才会发生变化,而CoreData的机制表明每一次数据库的变动都要改动版本标识,那么我通过缓存和查询版本标识的变动来确定何时进行备份,避免频繁备份。

备份文件有效性:

既然 DB 可以损坏,那么这个备份文件也会损坏,怎么办呢?我用了双备份,每一个版本备份两个文件,如果一个备份恢复失败,就会启动另一个备份文件恢复。

介入恢复时机:

当CoreData初始化SQLite前,校验SQLite的Head完整性,如果不完整,进行介入修复。

经过我深入研究证明了这已经是最佳做法。

SQLite损坏修复相关推荐

  1. 微信 SQLite 数据库修复实践

    1.前言 众所周知,微信在后台服务器不保存聊天记录,微信在移动客户端所有的聊天记录都存储在一个 SQLite 数据库中,一旦这个数据库损坏,将会丢失用户多年的聊天记录.而我们监控到现网的损坏率是0.0 ...

  2. SQL数据库无法附加 系统表损坏修复 数据库中病毒解密恢复

    SQL数据库无法附加 系统表损坏修复 数据库中病毒解密恢复 开发此工具是为了 让手工恢复数据库物理故障时 更加简单便捷直观, 本工具用于物理修复独立处理大部分问题以及与DBCC配合完成修复各种数据库错 ...

  3. 只狼服务器维修或停机,只狼存档怎么替换 只狼存档损坏修复方法介绍_游侠网...

    想必很多玩只狼的朋友都碰到过存档损坏的情况吧,所以呢小编今天给大家带来的就是只狼存档损坏修复方法介绍,需要的朋友还不快进来看看. 存档损坏修复方法介绍 0.假设你要用A账号的存档(盗版的,别人的)替换 ...

  4. ubuntu系统损坏修复_修复损坏的ubuntu gui

    ubuntu系统损坏修复 I switch between work and Personal Computer a lot. I recently noticed that I haven't us ...

  5. linux sudoers文件损坏修复

    sudoers文件损坏修复--失败 教训:不要轻易修改这个文件 要有visudo不成功 pkexec visudo -f /etc/sudoers 使用su能进入root 重启ubuntu,随即长按s ...

  6. Linux系统的grub.cfg文件损坏修复

    Linux系统的grub.cfg文件损坏修复 一.grub.cfg文件介绍 1.grub.cfg文件位置 2.grub.cfg文件作用 3.系统启动流程介绍 二.grub.cfg文件损坏,系统开机启动 ...

  7. SONY索尼PXW-X280摄像机断电MXF/RSV视频文件损坏修复技术

    MXF视频文件简介 MXF是素材交换格式Material eXchange Format的首字母缩写.MXF是美国电影与电视工程师学会组织(SMPTE)定义的一种专业音视频媒体文件格式.MXF格式视频 ...

  8. SQL Server 损坏修复

     一 常见错误解读 SQL Server 对数据库损坏的错误类型做了细化,在此对几个典型的错误作一下介绍. 错误信息是:"在文件 '%ls'中.偏移量为 %#016I64x 的位置执行 ...

  9. 如何解决excel表格损坏修复中所遇到的难题

    今天不知道怎么了,excel文档突然就无法打开了.明明早上还是好好的,下午就不行了.连带企业邮箱里的附件都无法预览.就算可以打开时是空白的,但无法显示.重新下载后出现还是打不开.出现这一系列麻烦的是要 ...

最新文章

  1. 归档—监控ORACLE数据库告警日志
  2. python 模拟浏览器登录获取cookie_使用cookielib模拟浏览器在python中获取url
  3. python初步入门_python如何入门
  4. string.h包含哪些函数_多个函数组合拳专治不规则时间转化难题|Excel134
  5. [转载] 利用python制作简单计算器
  6. m_pRecordset-Open
  7. SET NOCOUNT
  8. 《2018华为92家核心供应商及其供应产品》
  9. Android实现淘宝购物车
  10. 央行降息 北上广深和厦门南京房价反弹可能性最大
  11. sql docker容器_如何将Microsoft SQL Server Docker容器与Azure Data Studio连接
  12. 超长攻略,机器学习基石!带你涉足王者之巅
  13. 简明理解 行列式和秩
  14. lzma打包exe_【原创】手写PE文件,打造史上最小LZMA解压DLL
  15. 在win20008上运行U890破解提示sorry,this application cannot run under a virtual machine
  16. 【干货收藏】 IGBT 的国产替代
  17. 移动学习 AndroidStudio内存优化分析—hprof文件分析
  18. 在vue中webSocket通信
  19. Ubuntu: AVI视频转MP4格式
  20. Python绘制地理图表之可视化神器pyecharts(三)

热门文章

  1. java毕业设计高校学生管理系统mybatis+源码+调试部署+系统+数据库+lw
  2. pc网站qq互联登录授权php版
  3. 2023基于微信小程序的游戏账号在线交易买卖平台(SSM+mysql)-JAVA.VUE(论文+开题报告+运行)
  4. mysql分库分表(一)
  5. 做什么职业,也别做程序员
  6. 解决fildder抓取localhost报文出错
  7. NVIDIA Jetson TX2
  8. Android 电源配置文件
  9. 找不到包 com.mapbar.android.location
  10. Keywords (关键字)