SQLite 数据库具有很强的抗损坏能力。在执行事务时如果发生应用程序崩溃、操作系统崩溃甚至电源故障,那么在下次访问数据库文件时,会自动回滚部分写入的事务。恢复过程是全自动的,不需要用户或应用程序的任何操作。尽管 SQLite 数据库具有很强的抗损坏能力,但仍有可能发生损坏。
数据库文件本身是磁盘文件的一种,因此任何进程都可以往这个文件中写入数据。SQLite 自身对这种行为也无能为力。
数据库文件关闭后又被开启,其他线程往旧的文件描述符写入数据,导致覆盖部分数据产生数据库损坏。
延伸:不同系统对于多进程写入同一个文件提供的处理能力。
在后台对数据库文件进行自动备份的时候,此时数据库可能处于事务之中。这个备份可能包含一些脏数据(旧的或者新的处于被更改的内容)。
实现可靠的数据库备份方式是使用 SQLite 提供的 backup API。当前一个事务失败时,将 journal 或 wal 日志文件与数据库文件一起拷贝非常重要。
SQLite通常将所有的内容存储在单个文件中。但在执行事务时,当产生崩溃或者异常断电,恢复数据库的必要信息保存在了一个辅助文件中,这个文件与数据库同名,并且添加了 journal 或者 wal 的文件后缀。当这个辅助文件被修改或者删除,那么数据库就有可能崩溃。
关于Hot Journals,官网是这么阐述的:
当 journal 日志或 wal 日志文件包含恢复数据库状态所需的信息时,它们被称为“热日志”或“热 WAL 文件”,通常出现在应用程序或者设备在事务完成之前崩溃。热日志和热 WAL 文件只是错误恢复场景中的一个因素,因此并不常见。但它们是 SQLite 数据库状态的一部分,因此不容忽视。
SQLite 数据库受数据库文件及日志文件共同控制,当两者受外部影响因素导致错误搭配时,可能导致数据库损坏。以下这些行为则可能导致数据库损坏:
SQLite 在数据库文件、WAL 文件上使用文件锁来协调并发进程之间的访问。如果没有加锁机制,多个线程或进程可能会尝试同时对数据库文件进行不兼容的更改,从而导致数据库损坏。
SQLite 依赖于底层文件系统对文件进行锁处理。但是一些文件系统在其锁逻辑中包含错误,因此文件加锁并不总是如预期表现。对于网络文件系统和 NFS 尤其如此。如果在锁定原语包含错误的文件系统上使用 SQLite,并且如果多个线程或进程尝试同时访问同一个数据库,则可能导致数据库损坏。
为了保证数据库文件始终保持一致,SQLite 偶尔会要求操作系统将所有挂起的写入刷新到持久存储,然后等待刷新完成。这是使用 unix 下的 fsync() 系统调用和 Windows 下的 FlushFileBuffers() 来完成的。我们将这种挂起的写入刷新称为“同步”。 实际上,如果一个人只关心原子性和一致性写入并且愿意放弃持久性写入,那么同步操作不需要等到内容完全存储在持久性媒体上。相反,可以将同步操作视为 I/O 屏障。如果同步作为 I/O 屏障而不是真正的同步运行,则电源故障或系统崩溃可能会导致一个或多个先前提交的事务回滚(违反“ACID”的“持久”属性),但数据库至少会继续保持一致,这是大多数人关心的。
大多数消费级存储设备对于写入内容并不是严格同步的,当内容到达轨道缓冲区却还未被写入到磁盘时,设备驱动器就会反馈已经写入磁盘,这使得设备驱动器看起来运行得更快。在大部分时候,这种行为并没有什么不妥。但当内容到达轨道缓冲区却未写入磁盘,此时发生断电,那么数据库文件就可能发生损坏。相比较默认的日志模式,WAL 日志模式更能容忍乱序写入。在 WAL 模式下。如果在 checkpoint 期间出现同步失败,那么这将是导致数据库损坏的唯一原因。因此,防止由于同步失败导致的数据库损坏的一种方式是:在 WAL 日志模式,不要频繁的触发 checkpoint。
SQLite 确保完整性的同步操作可以在运行时使用 synchronous pragma 命令禁用。通过设置PRAGMA synchronous=OFF,所有同步操作都被省略。这使得 SQLite 看起来运行得更快,但它也允许操作系统自由地重新排序写入,如果在所有内容到达持久存储之前发生电源故障或硬重置,这可能会导致数据库损坏。
磁盘驱动器或闪存故障导致文件内容而发生更改,则 SQLite 数据库可能会损坏。虽然这种现象非常罕见,但磁盘仍可能意外翻转扇区中的一点导致故障产生。
SQLite 是一个 C 库,它与宿主应用运行在同一地址空间中。这意味着应用程序中的野指针、缓冲区溢出、堆损坏或其他故障可能会损坏 SQLite 内部的数据结构并最终导致数据库文件损坏。通常,这些类型的问题在发生任何数据库损坏之前都表现为段错误,但是在某些情况下,应用程序代码错误会导致 SQLite 发生故障,从而损坏数据库文件。
使用内存映射 I/O 时,内存损坏问题变得更加严重。当数据库文件的全部或部分映射到应用程序的地址空间时,覆盖该映射空间的任何部分的野指针将立即损坏数据库文件,而无需应用程序执行后续的 write() 系统调用。
SQLite 具有许多针对数据库损坏的内置保护。但是其中许多保护可以通过配置选项禁用。如果禁用保护,可能会发生数据库损坏。 以下是禁用 SQLite 内置保护机制的示例:
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_modeandroid.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命令通过解析sqlite_master表拿到所有的表信息,然后遍历每一张表的数据,对于每条记录输出一条相关的 SQLite 语句,当遇到错误无法解析出来则跳过继续解析下一张表。恢复的话对空 DB 文件执行输出的全部 SQLite 语句,这样就能恢复数据。这种方式可以提前对没有损坏的数据库文件执行.dump命令,起到备份恢复的作用。
// 查询完整的 sqlite_master 信息SELECT * FROM sqlite_master// 重定向到某个文件.output sqlite_dump.txt// dump数据库.dump
.dump命令也可以直接执行于损坏的数据库文件,当sqlite_master都无法读取时,将导致无法恢复任何数据。
SQLite自身提供的一套备份机制,按 Page 为单位复制到新 DB, 支持热备份。
WCDB 提供的修复方案,实际是自实现了B+树的解析逻辑,实现对数据的读取,补齐了备份恢复方案有时效性的缺点。并且由于大部分case(来自WCDB的统计数据)都是因为sqlite_master表损坏导致.dump方案失效,因此增加了对sqlite_mater的备份。而由于sqlite_master并不会频繁变更,只在表结构有变化时改变,因此可在升级时机覆盖备份。
如果本文对你有帮助,别忘记给我个3连 ,点赞,转发,评论,
学习更多JAVA知识与技巧,关注与私信博主(555)