SQLite 数据库具有很强的抗损坏能力。在执行事务时如果发生应用程序崩溃、操作系统崩溃甚至电源故障,那么在下次访问数据库文件时,会自动回滚部分写入的事务。恢复过程是全自动的,不需要用户或应用程序的任何操作。尽管 SQLite 数据库具有很强的抗损坏能力,但仍有可能发生损坏。
1. db文件被其他线程或进程破坏数据库文件本身是磁盘文件的一种,因此任何进程都可以往这个文件中写入数据。SQLite 自身对这种行为也无能为力。
1.1. 向已经关闭的文件描述符继续写入数据数据库文件关闭后又被开启,其他线程往旧的文件描述符写入数据,导致覆盖部分数据产生数据库损坏。
1.2 事务处于活跃状态下进行备份延伸:不同系统对于多进程写入同一个文件提供的处理能力。
在后台对数据库文件进行自动备份的时候,此时数据库可能处于事务之中。这个备份可能包含一些脏数据(旧的或者新的处于被更改的内容)。
实现可靠的数据库备份方式是使用 SQLite 提供的 backup API。当前一个事务失败时,将 journal 或 wal 日志文件与数据库文件一起拷贝非常重要。
1.3. 删除Hot Journals SQLite通常将所有的内容存储在单个文件中。但在执行事务时,当产生崩溃或者异常断电,恢复数据库的必要信息保存在了一个辅助文件中,这个文件与数据库同名,并且添加了 journal 或者 wal 的文件后缀。当这个辅助文件被修改或者删除,那么数据库就有可能崩溃。
关于Hot Journals,官网是这么阐述的:
1.4. 数据库文件与日志文件不一致当 journal 日志或 wal 日志文件包含恢复数据库状态所需的信息时,它们被称为“热日志”或“热 WAL 文件”,通常出现在应用程序或者设备在事务完成之前崩溃。热日志和热 WAL 文件只是错误恢复场景中的一个因素,因此并不常见。但它们是 SQLite 数据库状态的一部分,因此不容忽视。
SQLite 数据库受数据库文件及日志文件共同控制,当两者受外部影响因素导致错误搭配时,可能导致数据库损坏。以下这些行为则可能导致数据库损坏:
- 交换两个不同数据库的日志文件
- 将数据库日志文件复写为其他数据库的日志文件
- 将一个数据库的日志文件移动给其他数据库
- 覆盖数据库时却没有将其关联的日志文件一起删除
SQLite 在数据库文件、WAL 文件上使用文件锁来协调并发进程之间的访问。如果没有加锁机制,多个线程或进程可能会尝试同时对数据库文件进行不兼容的更改,从而导致数据库损坏。
2.1 文件系统的锁机制出问题或者未实现SQLite 依赖于底层文件系统对文件进行锁处理。但是一些文件系统在其锁逻辑中包含错误,因此文件加锁并不总是如预期表现。对于网络文件系统和 NFS 尤其如此。如果在锁定原语包含错误的文件系统上使用 SQLite,并且如果多个线程或进程尝试同时访问同一个数据库,则可能导致数据库损坏。
3. 同步失败为了保证数据库文件始终保持一致,SQLite 偶尔会要求操作系统将所有挂起的写入刷新到持久存储,然后等待刷新完成。这是使用 unix 下的 fsync() 系统调用和 Windows 下的 FlushFileBuffers() 来完成的。我们将这种挂起的写入刷新称为“同步”。 实际上,如果一个人只关心原子性和一致性写入并且愿意放弃持久性写入,那么同步操作不需要等到内容完全存储在持久性媒体上。相反,可以将同步操作视为 I/O 屏障。如果同步作为 I/O 屏障而不是真正的同步运行,则电源故障或系统崩溃可能会导致一个或多个先前提交的事务回滚(违反“ACID”的“持久”属性),但数据库至少会继续保持一致,这是大多数人关心的。
3.1 不遵守同步请求的设备驱动器大多数消费级存储设备对于写入内容并不是严格同步的,当内容到达轨道缓冲区却还未被写入到磁盘时,设备驱动器就会反馈已经写入磁盘,这使得设备驱动器看起来运行得更快。在大部分时候,这种行为并没有什么不妥。但当内容到达轨道缓冲区却未写入磁盘,此时发生断电,那么数据库文件就可能发生损坏。相比较默认的日志模式,WAL 日志模式更能容忍乱序写入。在 WAL 模式下。如果在 checkpoint 期间出现同步失败,那么这将是导致数据库损坏的唯一原因。因此,防止由于同步失败导致的数据库损坏的一种方式是:在 WAL 日志模式,不要频繁的触发 checkpoint。
3.2. 使用 PRAGMAs 禁用同步 SQLite 确保完整性的同步操作可以在运行时使用 synchronous pragma 命令禁用。通过设置PRAGMA synchronous=OFF
,所有同步操作都被省略。这使得 SQLite 看起来运行得更快,但它也允许操作系统自由地重新排序写入,如果在所有内容到达持久存储之前发生电源故障或硬重置,这可能会导致数据库损坏。
磁盘驱动器或闪存故障导致文件内容而发生更改,则 SQLite 数据库可能会损坏。虽然这种现象非常罕见,但磁盘仍可能意外翻转扇区中的一点导致故障产生。
5. 内存损坏 SQLite 是一个 C 库,它与宿主应用运行在同一地址空间中。这意味着应用程序中的野指针、缓冲区溢出、堆损坏或其他故障可能会损坏 SQLite 内部的数据结构并最终导致数据库文件损坏。通常,这些类型的问题在发生任何数据库损坏之前都表现为段错误,但是在某些情况下,应用程序代码错误会导致 SQLite 发生故障,从而损坏数据库文件。
使用内存映射 I/O 时,内存损坏问题变得更加严重。当数据库文件的全部或部分映射到应用程序的地址空间时,覆盖该映射空间的任何部分的野指针将立即损坏数据库文件,而无需应用程序执行后续的 write()
系统调用。
SQLite 具有许多针对数据库损坏的内置保护。但是其中许多保护可以通过配置选项禁用。如果禁用保护,可能会发生数据库损坏。 以下是禁用 SQLite 内置保护机制的示例:
- 设置 PRAGMA synchronous=OFF在出现操作系统崩溃或电源故障时可能导致数据库损坏(这个设置不会因为应用程序崩溃而损坏数据库)
- 当其他数据库连接打开时,改变 PRAGMA schema_version
- 使用 PRAGMA journal_mode=OFF 或 PRAGMA journal_mode=MEMORY 并在写入事务的中间应用程序崩溃。
- 设置 PRAGMA writable_schema=ON 然后使用 DML 语句更改数据库模式可能会使数据库完全不可读。
Android 基于 SQLite 提供了应用框架层的 API 供用户使用,当操作异常时,通过特定的 Exception 提示用户。一般有以下几种数据库错误:
数据库文件被异常删除android.database.sqlite.SQLiteDatabaseCorruptException: file is not a database (Sqlite code 26 SQLITE_NOTADB): , while compiling: PRAGMA journal_mode, (OS error - 2:No such file or directory)
android.database.sqlite.SQLiteConnection.nativePrepareStatement(SQLiteConnection.java)
android.database.sqlite.SQLiteConnection.acquirePreparedStatement(SQLiteConnection.java:1030)
android.database.sqlite.SQLiteConnection.executeForString(SQLiteConnection.java:773)
android.database.sqlite.SQLiteConnection.setJournalMode(SQLiteConnection.java:420)
android.database.sqlite.SQLiteConnection.setWalModeFromConfiguration(SQLiteConnection.java:334)
android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:238)
android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:211)
android.database.sqlite.SQLiteConnectionPool.openConnectionLocked(SQLiteConnectionPool.java:559)
android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:222)
android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:211)
android.database.sqlite.SQLiteDatabase.openInner(SQLiteDatabase.java:947)
android.database.sqlite.SQLiteDatabase.open(SQLiteDatabase.java:931)
android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:790)
android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:779)
android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:389)
android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:332)
android.arch.persistence.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.getWritableSupportDatabase(FrameworkSQLiteOpenHelper.java:96)
android.arch.persistence.db.framework.FrameworkSQLiteOpenHelper.getWritableDatabase(FrameworkSQLiteOpenHelper.java:54)
日志文件问题
android.database.sqlite.SQLiteDatabaseCorruptException: file is encrypted or is not a database (code 26): , while compiling: PRAGMA journal_mode
android.database.sqlite.SQLiteConnection.nativePrepareStatement(SQLiteConnection.java)
android.database.sqlite.SQLiteConnection.acquirePreparedStatement(SQLiteConnection.java:921)
android.database.sqlite.SQLiteConnection.executeForString(SQLiteConnection.java:648)
android.database.sqlite.SQLiteConnection.setJournalMode(SQLiteConnection.java:322)
android.database.sqlite.SQLiteConnection.setWalModeFromConfiguration(SQLiteConnection.java:293)
android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:217)
android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:195)
android.database.sqlite.SQLiteConnectionPool.openConnectionLocked(SQLiteConnectionPool.java:493)
android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:200)
android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:192)
android.database.sqlite.SQLiteDatabase.openInner(SQLiteDatabase.java:864)
android.database.sqlite.SQLiteDatabase.open(SQLiteDatabase.java:852)
android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:724)
android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:714)
android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:295)
android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:238)
android.arch.persistence.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.getWritableSupportDatabase(FrameworkSQLiteOpenHelper.java:96)
android.arch.persistence.db.framework.FrameworkSQLiteOpenHelper.getWritableDatabase(FrameworkSQLiteOpenHelper.java:54)
存储空间不足
android.database.sqlite.SQLiteFullException: database or disk is full (code 13 SQLITE_FULL)
android.database.sqlite.SQLiteConnection.nativeExecute(SQLiteConnection.java)
android.database.sqlite.SQLiteConnection.execute(SQLiteConnection.java:717)
android.database.sqlite.SQLiteSession.endTransactionUnchecked(SQLiteSession.java:439)
android.database.sqlite.SQLiteSession.endTransaction(SQLiteSession.java:403)
android.database.sqlite.SQLiteDatabase.endTransaction(SQLiteDatabase.java:592)
android.arch.persistence.db.framework.FrameworkSQLiteDatabase.endTransaction(FrameworkSQLiteDatabase.java:90)
android.database.sqlite.SQLiteDiskIOException: disk I/O error - SQLITE_IOERR_SHMSIZE (Sqlite code 4874): , while compiling: PRAGMA journal_mode, (OS error - 28:No space left on device)
android.database.sqlite.SQLiteConnection.nativePrepareStatement(SQLiteConnection.java)
android.database.sqlite.SQLiteConnection.acquirePreparedStatement(SQLiteConnection.java:927)
android.database.sqlite.SQLiteConnection.executeForString(SQLiteConnection.java:672)
android.database.sqlite.SQLiteConnection.setJournalMode(SQLiteConnection.java:358)
android.database.sqlite.SQLiteConnection.setWalModeFromConfiguration(SQLiteConnection.java:332)
android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:231)
android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:209)
android.database.sqlite.SQLiteConnectionPool.openConnectionLocked(SQLiteConnectionPool.java:541)
android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:209)
android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:198)
android.database.sqlite.SQLiteDatabase.openInner(SQLiteDatabase.java:936)
android.database.sqlite.SQLiteDatabase.open(SQLiteDatabase.java:920)
android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:795)
android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:785)
android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:307)
android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:250)
损坏修复
优化应用磁盘空间占用
应用迭代中,每个业务团队都有一些持久化的需求,然而大部分团队只管文件的创建,文件使用完后没有及时清理掉。如果不及时对各业务线文件创建进行监控和治理的话,会恶化由于空间不足导致的数据库异常。
除了 APP 本身对于磁盘空间的占用外,用户手机被其他文件占用导致磁盘空间满也是一大因素。因此,引导用户释放一定的空间也是一种方式。
通过一定的手段对数据库进行备份,同时为了减小备份的数据库文件对于磁盘空间的占用,进一步压缩备份文件。这种方案能够挽回一部分数据损失,主要取决于数据库损坏时备份的日志文件的时效性。
直接备份定期备份数据库及日志文件。当数据库损坏时,恢复备份的数据库文件。
.dump 命令 .dump
命令通过解析sqlite_master
表拿到所有的表信息,然后遍历每一张表的数据,对于每条记录输出一条相关的 SQLite 语句,当遇到错误无法解析出来则跳过继续解析下一张表。恢复的话对空 DB 文件执行输出的全部 SQLite 语句,这样就能恢复数据。这种方式可以提前对没有损坏的数据库文件执行.dump
命令,起到备份恢复的作用。
// 查询完整的 sqlite_master 信息
SELECT * FROM sqlite_master
// 重定向到某个文件
.output sqlite_dump.txt
// dump数据库
.dump
.dump
命令也可以直接执行于损坏的数据库文件,当sqlite_master
都无法读取时,将导致无法恢复任何数据。
SQLite自身提供的一套备份机制,按 Page 为单位复制到新 DB, 支持热备份。
RepairKit WCDB 提供的修复方案,实际是自实现了B+树的解析逻辑,实现对数据的读取,补齐了备份恢复方案有时效性的缺点。并且由于大部分case(来自WCDB的统计数据)都是因为sqlite_master
表损坏导致.dump
方案失效,因此增加了对sqlite_mater
的备份。而由于sqlite_master
并不会频繁变更,只在表结构有变化时改变,因此可在升级时机覆盖备份。
- How To Corrupt An SQLite Database File
- SQLite Result and Error Codes
- 微信 SQLite 数据库修复实践
- 微信移动端数据库组件WCDB系列(二) — 数据库修复三板斧
┆ 凉 ┆ 暖 ┆ 降 ┆ 等 ┆ 幸 ┆ 我 ┆ 我 ┆ 里 ┆ 将 ┆ ┆ 可 ┆ 有 ┆ 谦 ┆ 戮 ┆ 那 ┆ ┆ 大 ┆ ┆ 始 ┆ 然 ┆
┆ 薄 ┆ 一 ┆ 临 ┆ 你 ┆ 的 ┆ 还 ┆ 没 ┆ ┆ 来 ┆ ┆ 是 ┆ 来 ┆ 逊 ┆ 没 ┆ 些 ┆ ┆ 雁 ┆ ┆ 终 ┆ 而 ┆
┆ ┆ 暖 ┆ ┆ 如 ┆ 地 ┆ 站 ┆ 有 ┆ ┆ 也 ┆ ┆ 我 ┆ ┆ 的 ┆ 有 ┆ 精 ┆ ┆ 也 ┆ ┆ 没 ┆ 你 ┆
┆ ┆ 这 ┆ ┆ 试 ┆ 方 ┆ 在 ┆ 逃 ┆ ┆ 会 ┆ ┆ 在 ┆ ┆ 清 ┆ 来 ┆ 准 ┆ ┆ 没 ┆ ┆ 有 ┆ 没 ┆
┆ ┆ 生 ┆ ┆ 探 ┆ ┆ 最 ┆ 避 ┆ ┆ 在 ┆ ┆ 这 ┆ ┆ 晨 ┆ ┆ 的 ┆ ┆ 有 ┆ ┆ 来 ┆ 有 ┆
┆ ┆ 之 ┆ ┆ 般 ┆ ┆ 不 ┆ ┆ ┆ 这 ┆ ┆ 里 ┆ ┆ 没 ┆ ┆ 杀 ┆ ┆ 来 ┆ ┆ ┆ 来 ┆