数据库备份与恢复:sqlite3_backup_init API、在线备份、增量备份策略
说实话,数据库备份这事儿,我早年吃过亏。那时候刚用SQLite做嵌入式项目,觉得这小数据库轻量得很,直接拷文件不就完了?结果有一次程序跑着跑着突然断电,拷出来的文件打不开,数据全丢了。从那以后,我再也不敢小看备份这个环节。
今天咱们就聊聊SQLite的备份机制。核心API是sqlite3_backup_init,但光会用API还不够,你得理解在线备份和增量备份的策略选择。
一、为什么不能直接拷贝文件?
很多新手会问:SQLite数据库就是一个文件,我直接cp或者fread不就行了?嗯,理论上可以,但有个大坑——写操作未完成时拷贝,文件会损坏。
SQLite在写入时,会先写WAL(Write-Ahead Log)或回滚日志。如果你在事务中间拷贝文件,拷贝出来的可能是半成品。我见过一个案例,某设备每天凌晨备份数据库,结果有次备份时正好赶上自动清理旧数据,备份文件恢复后数据全乱了。
sqlite3_backup_*系列API才能保证一致性备份。
二、核心API:sqlite3_backup_init
这个API是SQLite 3.6.11版本引入的,专门解决在线备份问题。它的原理很简单:逐页拷贝。从源数据库读取每一页,写入目标数据库,同时处理并发写入。
函数签名如下:
sqlite3_backup *sqlite3_backup_init(
sqlite3 *pDest, // 目标数据库连接
const char *zDestName, // 目标数据库名称(通常是"main")
sqlite3 *pSource, // 源数据库连接
const char *zSourceName // 源数据库名称(通常是"main")
);
返回值是一个sqlite3_backup指针,后续操作都靠它。如果返回NULL,说明初始化失败,可以用sqlite3_errmsg查原因。
三、在线备份的完整流程
我个人习惯把备份流程拆成三步:初始化 → 逐页拷贝 → 收尾。下面是一个完整的示例:
#include <sqlite3.h>
#include <stdio.h>
int backup_database(const char *src_path, const char *dst_path) {
sqlite3 *pSrc = NULL, *pDst = NULL;
sqlite3_backup *pBackup = NULL;
int rc;
// 1. 打开源数据库
rc = sqlite3_open(src_path, &pSrc);
if (rc != SQLITE_OK) {
fprintf(stderr, "打开源数据库失败: %s\n", sqlite3_errmsg(pSrc));
return rc;
}
// 2. 创建目标数据库(如果不存在则创建)
rc = sqlite3_open(dst_path, &pDst);
if (rc != SQLITE_OK) {
fprintf(stderr, "打开目标数据库失败: %s\n", sqlite3_errmsg(pDst));
sqlite3_close(pSrc);
return rc;
}
// 3. 初始化备份
pBackup = sqlite3_backup_init(pDst, "main", pSrc, "main");
if (pBackup == NULL) {
fprintf(stderr, "备份初始化失败: %s\n", sqlite3_errmsg(pDst));
sqlite3_close(pSrc);
sqlite3_close(pDst);
return SQLITE_ERROR;
}
// 4. 逐页拷贝,每次拷贝5页
do {
rc = sqlite3_backup_step(pBackup, 5);
if (rc == SQLITE_OK || rc == SQLITE_DONE || rc == SQLITE_BUSY) {
// 可以在这里输出进度:sqlite3_backup_remaining() / sqlite3_backup_pagecount()
printf("备份进度: %d/%d 页\n",
sqlite3_backup_pagecount(pBackup) - sqlite3_backup_remaining(pBackup),
sqlite3_backup_pagecount(pBackup));
}
} while (rc == SQLITE_OK);
// 5. 完成备份
if (rc == SQLITE_DONE) {
printf("备份完成!\n");
} else {
fprintf(stderr, "备份过程中出错: %s\n", sqlite3_errmsg(pDst));
}
// 6. 清理资源
sqlite3_backup_finish(pBackup);
sqlite3_close(pSrc);
sqlite3_close(pDst);
return (rc == SQLITE_DONE) ? SQLITE_OK : rc;
}
sqlite3_backup_step的第二个参数是每次拷贝的页数。传5表示每次拷贝5页,这样可以在拷贝过程中让出CPU,给其他写操作机会。如果你传-1,则一次性拷贝所有页,速度最快但会长时间锁定源数据库。
四、在线备份的核心机制
你可能会问:拷贝过程中如果有写入怎么办?这就是sqlite3_backup_* API的精妙之处。
它内部维护了一个页锁机制:
- 拷贝某页时,对该页加读锁
- 拷贝完成后释放锁
- 如果某页在拷贝过程中被修改,下次
sqlite3_backup_step会重新拷贝该页
说白了,它保证最终一致性——所有在备份开始前提交的事务都会出现在备份中,备份开始后提交的事务可能出现在备份中,也可能不出现。但无论如何,备份文件本身是一致且可用的。
我曾经在项目中用这个API做热备,数据库文件有2GB,备份过程中业务写入从未停止。最终备份出来的文件,用PRAGMA integrity_check检查,全部通过。
五、增量备份策略
全量备份虽然简单,但数据量大时效率低。比如一个10GB的数据库,每天全量备份一次,磁盘和带宽都扛不住。这时候就需要增量备份。
SQLite本身没有内置增量备份API,但我们可以利用WAL模式和修改时间戳来实现。
我常用的策略有两种:
| 策略 | 原理 | 适用场景 |
|---|---|---|
| WAL日志备份 | 备份WAL文件中的增量数据 | 写入频繁,但每次写入量小 |
| 时间戳对比 | 记录每张表的最后修改时间,只备份变化的数据 | 数据量巨大,但变化范围小 |
5.1 WAL日志备份法
当SQLite开启WAL模式时,所有写入先进入WAL文件,然后异步合并到主数据库。我们可以定期备份WAL文件,再配合全量备份恢复。
步骤:
- 第一次做全量备份(用
sqlite3_backup_init) - 后续每次备份,只拷贝WAL文件(
数据库名-wal) - 恢复时,先恢复全量备份,再应用WAL文件
sqlite3_wal_checkpoint或直接打开数据库让SQLite自动合并。
5.2 时间戳对比法
这个方案更灵活,适合自定义场景。我在一个物联网项目中用过:每张表都有一个last_modified字段,备份时只导出最近修改过的记录。
-- 创建带时间戳的表
CREATE TABLE sensor_data (
id INTEGER PRIMARY KEY,
value REAL NOT NULL,
timestamp TEXT NOT NULL DEFAULT (datetime('now'))
);
-- 增量备份:只导出最近1小时的数据
ATTACH DATABASE 'incremental_backup.db' AS backup;
CREATE TABLE backup.sensor_data AS
SELECT * FROM main.sensor_data
WHERE timestamp > datetime('now', '-1 hour');
DETACH DATABASE backup;
这种方式的优点是粒度细,可以精确到行级别。缺点是需要在应用层维护时间戳,而且删除操作需要额外处理(比如用软删除标记)。
六、备份策略的选择建议
说了这么多,到底该用哪种?我根据项目经验总结了一张表:
| 场景 | 推荐策略 | 原因 |
|---|---|---|
| 数据库 < 100MB | 全量在线备份 | 简单可靠,几秒搞定 |
| 数据库 100MB ~ 1GB | 全量备份 + WAL增量 | 平衡效率与复杂度 |
| 数据库 > 1GB | 时间戳增量备份 | 避免频繁拷贝大文件 |
| 嵌入式设备(低内存) | 全量备份,每次拷贝1页 | 防止内存暴涨 |
你想想看,如果你的设备只有64MB内存,却要备份一个500MB的数据库,一次性拷贝所有页会怎样?内存直接爆掉。所以嵌入式场景下,我建议sqlite3_backup_step的页数参数设小一点,比如1或2。
七、备份恢复的避坑指南
我曾经在恢复备份时踩过一个坑:备份文件拷到另一台设备上,打开后数据正常,但PRAGMA user_version丢失了。后来才发现,sqlite3_backup_init默认只拷贝表数据和索引,不会拷贝元数据(如user_version、schema_version)。
解决方案是:备份完成后,手动读取并写入这些元数据。
// 备份完成后,额外保存元数据
int user_ver = sqlite3_exec(pSrc, "PRAGMA user_version", ...);
char sql[64];
snprintf(sql, sizeof(sql), "PRAGMA user_version = %d", user_ver);
sqlite3_exec(pDst, sql, NULL, NULL, NULL);
另一个坑是文件路径权限。嵌入式设备上,目标路径可能没有写权限。我建议在调用sqlite3_open之前,先用access()检查路径可写性。
八、知识体系总览
下面这张图总结了本章的核心逻辑,从备份方式到恢复流程,一目了然:
嗯,备份这事儿,说难不难,说简单也不简单。核心就一句话:别等到数据丢了才想起备份。我建议你在项目初期就把备份模块写好,哪怕只是每天全量备份一次,也比没有强。
最后提醒一句:备份文件一定要做异地存储。我曾经见过有人把备份文件放在同一块硬盘上,硬盘坏了,主库和备份一起消失。那叫一个欲哭无泪。