数据库备份与恢复:别让数据丢了才后悔

说实话,做Android开发这么多年,我最怕听到的一句话就是:「用户的数据丢了」。尤其是SQLite这种本地数据库,一旦出问题,用户骂娘,我们背锅。今天咱们就好好聊聊数据库备份与恢复这件事。我自己的经验是——备份方案一定要提前设计好,别等上线了再补,那可就晚了。

SQLite数据库文件备份:最朴素也最有效

SQLite本质上就是一个文件。所以最简单的备份方式,就是直接复制这个文件。但这里有个坑——数据库可能正在被读写,直接复制会出问题。

注意:千万不要在数据库打开状态下直接复制文件。你可能会拿到一个损坏的副本。

我个人的习惯做法是:先关闭数据库连接,再复制文件。或者用SQLite提供的备份API。

// 关闭数据库后复制文件
fun backupDatabase(context: Context, dbName: String, backupPath: String) {
    val dbFile = context.getDatabasePath(dbName)
    if (!dbFile.exists()) return
    
    // 先关闭所有数据库连接
    // 注意:RoomDatabase.close() 会关闭所有连接
    val backupFile = File(backupPath)
    dbFile.inputStream().use { input ->
        backupFile.outputStream().use { output ->
            input.copyTo(output)
        }
    }
}

嗯,这里要注意一点。如果你的App有多个地方同时访问数据库,最好加个锁机制。我曾经在一个项目中遇到过并发备份导致数据不一致的问题,后来改用SQLiteDatabase.openDatabase的只读模式来解决。

Android Backup API:系统级的备份方案

Android从早期版本就提供了备份机制。说白了,就是让系统帮你把数据备份到云端。但这里有个限制——不同厂商的实现不一样,有些手机甚至不支持。

我建议你使用BackupAgentHelper或者自定义BackupAgent。Room数据库的话,更推荐用FileBackupHelper

class MyBackupAgent : BackupAgentHelper() {
    override fun onCreate() {
        // 注册需要备份的数据库文件
        addHelper("database", FileBackupHelper(this, 
            "../databases/my_database.db",
            "../databases/my_database.db-wal",
            "../databases/my_database.db-shm"
        ))
    }
}
小技巧:记得把WAL和SHM文件也一起备份。否则恢复时可能遇到「数据库损坏」的错误。我踩过这个坑,折腾了两天才找到原因。

Room数据库导出/导入:更优雅的方式

Room本身没有提供直接的导出导入API。但我们可以利用它底层的SQLiteDatabase来实现。我个人比较喜欢用SupportSQLiteDatabasebackup方法。

// 导出Room数据库
fun exportRoomDatabase(roomDatabase: RoomDatabase, outputFile: File) {
    roomDatabase.openHelper.writableDatabase.apply {
        // 使用SQLite的备份API
        val backup = SQLiteDatabase.openDatabase(
            outputFile.absolutePath, 
            null, 
            SQLiteDatabase.OPEN_READWRITE or SQLiteDatabase.CREATE_IF_NECESSARY
        )
        val backupConfig = SQLiteDatabase.BackupHelper(this, backup)
        backupConfig.backup(backup)
        backup.close()
    }
}

// 导入Room数据库
fun importRoomDatabase(roomDatabase: RoomDatabase, inputFile: File) {
    // 先关闭Room数据库
    roomDatabase.close()
    
    // 替换数据库文件
    val dbPath = roomDatabase.openHelper.writableDatabase.path
    val targetFile = File(dbPath)
    inputFile.inputStream().use { input ->
        targetFile.outputStream().use { output ->
            input.copyTo(output)
        }
    }
    
    // 重新打开数据库
    // 注意:这里需要重新创建RoomDatabase实例
}

你想想看,这种方式的好处是什么?它不依赖系统服务,完全由你控制。而且备份出来的文件是标准的SQLite格式,可以用任何SQLite工具打开查看。

云备份策略:让数据跟着用户走

本地备份再安全,手机丢了也是白搭。所以云备份是必须的。我常用的方案有两种:

  • Firebase Backup:Google自家的方案,集成简单,但国内访问不稳定
  • 自建云备份:用你自己的服务器,可控性高

我个人更倾向于自建方案。为什么呢?因为你可以自定义备份频率、压缩方式、加密策略。举个例子:

// 云备份的核心逻辑
fun uploadBackupToCloud(context: Context, dbName: String) {
    val dbFile = context.getDatabasePath(dbName)
    
    // 1. 先压缩
    val zipFile = File(context.cacheDir, "${dbName}_backup.zip")
    ZipOutputStream(zipFile.outputStream()).use { zos ->
        zos.putNextEntry(ZipEntry(dbName))
        dbFile.inputStream().use { it.copyTo(zos) }
        zos.closeEntry()
    }
    
    // 2. 再加密(AES-256)
    val encryptedData = encryptFile(zipFile, getEncryptionKey())
    
    // 3. 上传到你的服务器
    uploadToServer(encryptedData, getUserId())
}
核心要点:云备份一定要加密。用户数据是敏感信息,泄露了可不是闹着玩的。我见过太多因为没加密导致数据泄露的案例了。

增量备份方案:省流量省时间

全量备份虽然简单,但数据量大了之后,每次备份都上传整个文件,太浪费了。增量备份只备份变化的部分,效率高得多。

SQLite的WAL(Write-Ahead Logging)机制天然支持增量备份。说白了,WAL文件里记录的就是最近的变化。我们只需要备份WAL文件就行了。

// 增量备份:只备份WAL文件
fun incrementalBackup(context: Context, dbName: String) {
    val walFile = context.getDatabasePath("${dbName}-wal")
    val shmFile = context.getDatabasePath("${dbName}-shm")
    
    // 检查WAL文件是否有变化
    if (walFile.exists() && walFile.lastModified() > lastBackupTime) {
        // 只备份WAL和SHM文件
        backupFile(walFile, getBackupDir())
        backupFile(shmFile, getBackupDir())
        
        // 记录备份时间
        lastBackupTime = System.currentTimeMillis()
    }
}

// 恢复时:先恢复主数据库,再应用WAL
fun restoreWithIncremental(context: Context, dbName: String) {
    // 1. 恢复主数据库文件
    restoreMainDatabase(dbName)
    
    // 2. 应用WAL文件
    val walBackup = getBackupFile("${dbName}-wal")
    if (walBackup.exists()) {
        walBackup.copyTo(context.getDatabasePath("${dbName}-wal"), true)
    }
    
    // 3. 打开数据库时会自动合并WAL
    val db = SQLiteDatabase.openDatabase(
        context.getDatabasePath(dbName).absolutePath,
        null,
        SQLiteDatabase.OPEN_READWRITE
    )
    db.close()
}

为什么会推荐增量备份?举个例子你就明白了。假设你的数据库有100MB,每天只修改了几条记录。全量备份每次都要上传100MB,而增量备份可能只需要几KB。用户流量省了,服务器压力也小了。

知识体系总览

下面这张图是我自己整理的备份恢复知识体系,你可以对照着看看自己哪些地方还没覆盖到。

数据库备份与恢复 SQLite文件备份 直接复制数据库文件 注意关闭连接 处理WAL/SHM文件 Android Backup API 系统级备份方案 BackupAgentHelper 依赖厂商支持 Room导出/导入 使用SQLite备份API 可跨设备迁移 标准SQLite格式 云备份策略 Firebase/自建 压缩+加密 定期上传 增量备份方案 基于WAL机制 只备份变化部分 省流量省时间 数据恢复策略 全量恢复 增量恢复 数据一致性检查 选择适合你业务的备份方案,别让数据丢了才后悔

避坑指南

最后,分享几个我踩过的坑,希望能帮你少走弯路:

  • 备份文件不要放在App私有目录:用户卸载App时会被清空。我建议放在外部存储或者云上。
  • 恢复数据前一定要校验完整性:PRAGMA integrity_check检查一下。我曾经恢复了一个损坏的备份,结果用户数据全乱了。
  • 考虑多线程并发问题:备份和读写操作不要同时进行。用ReentrantLock或者Room的事务机制来控制。
  • 大数据库备份要显示进度:用户最怕的就是App卡住不动。给个进度条,用户心里踏实。

好了,关于数据库备份与恢复,今天就聊这么多。记住一句话:备份不是可选项,是必选项。你永远不知道用户什么时候会摔手机。

公众号:蓝海资料掘金营,微信deep3321