数据库备份与恢复:sqlite3_backup_init系列函数、在线备份、增量备份策略

说实话,数据库备份这事儿,我早年吃过亏。有一次线上服务跑得好好的,突然磁盘坏了,整个数据库文件打不开。那时候我才意识到——备份不是可选项,是必选项。今天咱们就聊聊 SQLite 的备份机制,尤其是 sqlite3_backup_init 这套 API。

一、SQLite 备份的基本思路

SQLite 的备份,说白了就是把一个数据库拷贝到另一个地方。但问题在于——数据库可能正在被写入。你直接 cp 文件,万一写到一半,拷贝出来的文件就坏了。

SQLite 官方提供了一套在线备份 API,核心就是 sqlite3_backup_initsqlite3_backup_stepsqlite3_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 小时服务
离线备份完全无干扰需要停服务可维护窗口内

六、备份流程示意图

下面这张图,展示了我常用的备份流程。说白了就是:先判断要不要全量,再决定增量怎么走。

开始备份 是否到全量备份时间? (例如:每天凌晨2点) 执行全量备份 sqlite3_backup_step(…, -1) 执行增量备份 仅备份变化的WAL页 验证备份文件完整性 PRAGMA integrity_check 备份完成

七、避坑指南

这些年我踩过的坑不少,挑几个典型的说说:

  • 备份文件被占用:有一次备份到一半,目标文件被杀毒软件锁住了,结果备份失败。后来我改成先备份到临时文件,再 rename 过去。
  • 磁盘空间不足:全量备份时,如果磁盘满了,备份会失败。我建议备份前先检查磁盘剩余空间。
  • WAL 文件没清理:增量备份依赖 WAL 文件,但如果 WAL 文件一直不 checkpoint,会越来越大。我习惯每次全量备份后,手动执行一次 checkpoint。
  • 备份超时:如果数据库特别大,全量备份可能耗时很久。我建议设置一个超时机制,超过 30 分钟就报警。

我的经验:备份这事儿,宁可多备份几次,也不要漏掉一次。我曾经因为偷懒,把增量备份间隔从 15 分钟改成 1 小时,结果刚好在那一小时内出了故障,丢了 40 分钟的数据。从那以后,我再也不敢随便调长间隔了。

八、总结

SQLite 的备份 API 其实不复杂,核心就是 sqlite3_backup_init + sqlite3_backup_step + sqlite3_backup_finish。但真正用好它,需要结合业务场景选择合适的策略——全量还是增量?在线还是离线?间隔多久?这些都得根据你的数据量和可用性要求来定。

嗯,备份就像买保险——平时觉得没用,真出事的时候才知道它有多重要。希望今天的分享能帮你少走一些弯路。

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