数据库备份与恢复:别让数据丢了才后悔
说实话,做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"
))
}
}
Room数据库导出/导入:更优雅的方式
Room本身没有提供直接的导出导入API。但我们可以利用它底层的SQLiteDatabase来实现。我个人比较喜欢用SupportSQLiteDatabase的backup方法。
// 导出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。用户流量省了,服务器压力也小了。
知识体系总览
下面这张图是我自己整理的备份恢复知识体系,你可以对照着看看自己哪些地方还没覆盖到。
避坑指南
最后,分享几个我踩过的坑,希望能帮你少走弯路:
- 备份文件不要放在App私有目录:用户卸载App时会被清空。我建议放在外部存储或者云上。
- 恢复数据前一定要校验完整性:用
PRAGMA integrity_check检查一下。我曾经恢复了一个损坏的备份,结果用户数据全乱了。 - 考虑多线程并发问题:备份和读写操作不要同时进行。用
ReentrantLock或者Room的事务机制来控制。 - 大数据库备份要显示进度:用户最怕的就是App卡住不动。给个进度条,用户心里踏实。
好了,关于数据库备份与恢复,今天就聊这么多。记住一句话:备份不是可选项,是必选项。你永远不知道用户什么时候会摔手机。