数据库备份与恢复:sqlite3_backup_init系列函数、在线备份、增量备份策略
说实话,数据库备份这事儿,我早年吃过亏。有一次线上服务跑得好好的,突然磁盘坏了,整个数据库文件打不开。那时候我才意识到——备份不是可选项,是必选项。今天咱们就聊聊 SQLite 的备份机制,尤其是 sqlite3_backup_init 这套 API。
一、SQLite 备份的基本思路
SQLite 的备份,说白了就是把一个数据库拷贝到另一个地方。但问题在于——数据库可能正在被写入。你直接 cp 文件,万一写到一半,拷贝出来的文件就坏了。
SQLite 官方提供了一套在线备份 API,核心就是 sqlite3_backup_init、sqlite3_backup_step、sqlite3_backup_finish 这三个函数。它们能在数据库运行时,安全地做一份快照。
核心概念:在线备份 = 数据库还在服务,你一边读一边写,备份工具在背后悄悄拷贝页面。拷贝过程中,源库的写入会被锁住一小会儿,但整体服务不会中断。
二、sqlite3_backup_init 系列函数详解
这套 API 其实就三个步骤:初始化、逐步拷贝、收尾。我习惯把它们叫做「备份三步曲」。
2.1 初始化:sqlite3_backup_init
sqlite3_backup *sqlite3_backup_init(
sqlite3 *pDest, // 目标数据库连接
const char *zDestName, // 目标数据库名称(通常是 "main")
sqlite3 *pSource, // 源数据库连接
const char *zSourceName // 源数据库名称(通常是 "main")
);
这个函数会创建一个备份对象。如果返回 NULL,说明初始化失败,可以用 sqlite3_errcode 查原因。我在项目中遇到过一种情况——目标数据库文件权限不对,结果初始化直接返回 NULL,排查了半天才发现是文件权限问题。
2.2 逐步拷贝:sqlite3_backup_step
int sqlite3_backup_step(sqlite3_backup *p, int nPage);
参数 nPage 表示一次拷贝多少页。传 -1 表示一次性拷贝完。但如果你希望备份过程中不阻塞太久,可以每次拷贝少量页面,比如 100 页。
返回值说明:
| 返回值 | 含义 |
|---|---|
| SQLITE_OK | 还有页面需要拷贝,继续调用 |
| SQLITE_DONE | 所有页面拷贝完成 |
| SQLITE_BUSY | 源数据库被其他操作锁住了 |
| SQLITE_LOCKED | 目标数据库被锁住了 |
| 其他错误码 | 出错了,需要调用 finish 清理 |
小技巧:我一般每次传 100 页,然后 sleep 10 毫秒,这样既能保证备份进度,又不会把数据库锁太久。你想想看,如果一次拷贝几万页,其他写操作就得等很久。
2.3 收尾:sqlite3_backup_finish
int sqlite3_backup_finish(sqlite3_backup *p);
不管备份成功还是失败,最后都要调用这个函数释放资源。如果备份过程中出错了,finish 会返回错误码,但资源一定会释放。
三、完整的在线备份示例
下面这段代码,是我在实际项目中用过的模板。嗯,稍微简化了一下,但核心逻辑没变。
#include <sqlite3.h>
#include <stdio.h>
int online_backup(const char *src_path, const char *dst_path) {
sqlite3 *pSrc = NULL, *pDst = NULL;
sqlite3_backup *pBackup = NULL;
int rc;
// 打开源数据库
rc = sqlite3_open(src_path, &pSrc);
if (rc != SQLITE_OK) {
fprintf(stderr, "打开源数据库失败: %s\n", sqlite3_errmsg(pSrc));
return rc;
}
// 打开目标数据库(会创建新文件)
rc = sqlite3_open(dst_path, &pDst);
if (rc != SQLITE_OK) {
fprintf(stderr, "打开目标数据库失败: %s\n", sqlite3_errmsg(pDst));
sqlite3_close(pSrc);
return rc;
}
// 初始化备份
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;
}
// 逐步拷贝,每次100页
do {
rc = sqlite3_backup_step(pBackup, 100);
if (rc == SQLITE_OK || rc == SQLITE_DONE) {
// 正常进度
int remaining = sqlite3_backup_remaining(pBackup);
int pagecount = sqlite3_backup_pagecount(pBackup);
printf("备份进度: %d/%d 页\n", pagecount - remaining, pagecount);
}
if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED) {
// 被锁住了,等一会儿再试
sqlite3_sleep(100);
}
} while (rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED);
// 收尾
rc = sqlite3_backup_finish(pBackup);
if (rc != SQLITE_OK) {
fprintf(stderr, "备份完成但存在错误: %s\n", sqlite3_errmsg(pDst));
}
sqlite3_close(pSrc);
sqlite3_close(pDst);
return (rc == SQLITE_OK) ? SQLITE_OK : SQLITE_ERROR;
}
注意:备份过程中,源数据库的写操作会被短暂阻塞。如果你对延迟非常敏感,建议每次只拷贝少量页面,并且把 sleep 时间调长一点。我曾经在一个高并发系统上,因为一次拷贝 1000 页导致写请求超时,后来改成 50 页就没事了。
四、增量备份策略
全量备份虽然简单,但数据量大了以后,每次都拷贝整个文件太慢了。这时候就需要增量备份。
SQLite 本身没有内置的增量备份 API,但我们可以利用 WAL(Write-Ahead Log)模式来实现。说白了,就是只备份从上次备份以来发生变化的数据页。
思路是这样的:
- 开启 WAL 模式,数据库会生成一个
.wal文件 - 每次备份时,先检查
.wal文件的大小 - 如果
.wal文件有变化,说明有新的写入,只备份这些变化 - 定期做一次全量备份,然后清空 WAL 文件
我个人的习惯是:每天凌晨做一次全量备份,白天每 15 分钟做一次增量备份。这样即使出问题,最多丢失 15 分钟的数据。
五、备份策略对比
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 全量备份 | 简单可靠,恢复快 | 耗时长,占用空间大 | 数据量小,或低频备份 |
| 增量备份 | 速度快,节省空间 | 恢复时需要全量+增量合并 | 数据量大,高频备份 |
| 在线备份 | 不中断服务 | 会短暂阻塞写操作 | 7x24 小时服务 |
| 离线备份 | 完全无干扰 | 需要停服务 | 可维护窗口内 |
六、备份流程示意图
下面这张图,展示了我常用的备份流程。说白了就是:先判断要不要全量,再决定增量怎么走。
七、避坑指南
这些年我踩过的坑不少,挑几个典型的说说:
- 备份文件被占用:有一次备份到一半,目标文件被杀毒软件锁住了,结果备份失败。后来我改成先备份到临时文件,再 rename 过去。
- 磁盘空间不足:全量备份时,如果磁盘满了,备份会失败。我建议备份前先检查磁盘剩余空间。
- WAL 文件没清理:增量备份依赖 WAL 文件,但如果 WAL 文件一直不 checkpoint,会越来越大。我习惯每次全量备份后,手动执行一次 checkpoint。
- 备份超时:如果数据库特别大,全量备份可能耗时很久。我建议设置一个超时机制,超过 30 分钟就报警。
我的经验:备份这事儿,宁可多备份几次,也不要漏掉一次。我曾经因为偷懒,把增量备份间隔从 15 分钟改成 1 小时,结果刚好在那一小时内出了故障,丢了 40 分钟的数据。从那以后,我再也不敢随便调长间隔了。
八、总结
SQLite 的备份 API 其实不复杂,核心就是 sqlite3_backup_init + sqlite3_backup_step + sqlite3_backup_finish。但真正用好它,需要结合业务场景选择合适的策略——全量还是增量?在线还是离线?间隔多久?这些都得根据你的数据量和可用性要求来定。
嗯,备份就像买保险——平时觉得没用,真出事的时候才知道它有多重要。希望今天的分享能帮你少走一些弯路。