小巧。快速。可靠。
三选二。

使用 SQLite 在线备份 API

历史上,SQLite 数据库的备份(副本)是使用以下方法创建的

  1. 使用 SQLite API(例如 shell 工具)在数据库文件上建立共享锁。
  2. 使用外部工具(例如 unix 'cp' 实用程序或 DOS 'copy' 命令)复制数据库文件。
  3. 放弃在步骤 1 中获得的数据库文件的共享锁。

此过程在许多情况下运行良好,通常非常快。但是,此技术有以下缺点

在线备份 API 的创建是为了解决这些问题。在线备份 API 允许将一个数据库的内容复制到另一个数据库文件,替换目标数据库的任何原始内容。复制操作可以增量进行,在这种情况下,源数据库不需要在整个复制过程中锁定,而只需要在实际读取数据时短暂锁定。这允许其他数据库用户在进行在线数据库备份时继续使用,而不会出现过多的延迟。

完成备份调用序列的效果是使目标成为源数据库在开始复制时的逐位相同副本。(目标成为“快照”。)

在线备份 API 的 文档在这里。本页的其余部分包含两个 C 语言示例,说明 API 的常见用法及其讨论。阅读这些示例不能替代阅读 API 文档!

更新:在 SQLite 版本 3.27.0 (2019-02-07) 中引入的 VACUUM INTO 命令可以用作备份 API 的替代方法。

示例 1:加载和保存内存数据库

/*
** This function is used to load the contents of a database file on disk 
** into the "main" database of open database connection pInMemory, or
** to save the current contents of the database opened by pInMemory into
** a database file on disk. pInMemory is probably an in-memory database, 
** but this function will also work fine if it is not.
**
** Parameter zFilename points to a nul-terminated string containing the
** name of the database file on disk to load from or save to. If parameter
** isSave is non-zero, then the contents of the file zFilename are 
** overwritten with the contents of the database opened by pInMemory. If
** parameter isSave is zero, then the contents of the database opened by
** pInMemory are replaced by data loaded from the file zFilename.
**
** If the operation is successful, SQLITE_OK is returned. Otherwise, if
** an error occurs, an SQLite error code is returned.
*/
int loadOrSaveDb(sqlite3 *pInMemory, const char *zFilename, int isSave){
  int rc;                   /* Function return code */
  sqlite3 *pFile;           /* Database connection opened on zFilename */
  sqlite3_backup *pBackup;  /* Backup object used to copy data */
  sqlite3 *pTo;             /* Database to copy to (pFile or pInMemory) */
  sqlite3 *pFrom;           /* Database to copy from (pFile or pInMemory) */

  /* Open the database file identified by zFilename. Exit early if this fails
  ** for any reason. */
  rc = sqlite3_open(zFilename, &pFile);
  if( rc==SQLITE_OK ){

    /* If this is a 'load' operation (isSave==0), then data is copied
    ** from the database file just opened to database pInMemory. 
    ** Otherwise, if this is a 'save' operation (isSave==1), then data
    ** is copied from pInMemory to pFile.  Set the variables pFrom and
    ** pTo accordingly. */
    pFrom = (isSave ? pInMemory : pFile);
    pTo   = (isSave ? pFile     : pInMemory);

    /* Set up the backup procedure to copy from the "main" database of 
    ** connection pFile to the main database of connection pInMemory.
    ** If something goes wrong, pBackup will be set to NULL and an error
    ** code and message left in connection pTo.
    **
    ** If the backup object is successfully created, call backup_step()
    ** to copy data from pFile to pInMemory. Then call backup_finish()
    ** to release resources associated with the pBackup object.  If an
    ** error occurred, then an error code and message will be left in
    ** connection pTo. If no error occurred, then the error code belonging
    ** to pTo is set to SQLITE_OK.
    */
    pBackup = sqlite3_backup_init(pTo, "main", pFrom, "main");
    if( pBackup ){
      (void)sqlite3_backup_step(pBackup, -1);
      (void)sqlite3_backup_finish(pBackup);
    }
    rc = sqlite3_errcode(pTo);
  }

  /* Close the database connection opened on database file zFilename
  ** and return the result of this function. */
  (void)sqlite3_close(pFile);
  return rc;
}

右侧的 C 函数演示了备份 API 最简单、最常见的用途之一:将内存数据库的内容加载和保存到磁盘上的文件。在本示例中,备份 API 的使用方法如下

  1. 调用函数 sqlite3_backup_init() 来创建一个 sqlite3_backup 对象,用于在两个数据库之间复制数据(从文件到内存数据库,反之亦然)。
  2. 调用函数 sqlite3_backup_step(),参数为-1将整个源数据库复制到目标。
  3. 调用函数 sqlite3_backup_finish() 来清理 sqlite3_backup_init() 分配的资源。

错误处理

如果三个主要备份 API 例程中的任何一个出现错误,则 错误代码消息 将附加到目标 数据库连接。此外,如果 sqlite3_backup_step() 遇到错误,则 错误代码 将由 sqlite3_backup_step() 调用本身和随后的 sqlite3_backup_finish() 调用返回。因此,对 sqlite3_backup_finish() 的调用不会覆盖由 sqlite3_backup_step() 存储在目标 数据库连接 中的 错误代码。此功能用于示例代码以减少所需的错误处理量。忽略 sqlite3_backup_step()sqlite3_backup_finish() 调用的返回值,并从目标 数据库连接 中收集指示复制操作成功或失败的错误代码。

可能的增强功能

此函数的实现至少可以通过两种方式增强

  1. 无法获得数据库文件 zFilename 的锁(SQLITE_BUSY 错误)可以处理,并且
  2. 可以更好地处理数据库 pInMemory 和 zFilename 的页面大小不同的情况。

由于数据库 zFilename 是磁盘上的文件,因此它可能被另一个进程从外部访问。这意味着当调用 sqlite3_backup_step() 尝试从其读取或写入数据时,它可能无法获得所需的文件锁。如果发生这种情况,此实现将失败,立即返回 SQLITE_BUSY。解决方案是在打开 数据库连接 pFile 后立即使用 sqlite3_busy_handler()sqlite3_busy_timeout() 注册一个繁忙处理程序回调或超时。如果它无法立即获得所需的锁,sqlite3_backup_step() 使用任何已注册的繁忙处理程序回调或超时,就像 sqlite3_step()sqlite3_exec() 一样。

通常,在覆盖目标内容之前,源数据库和目标数据库的页面大小是否不同并不重要。目标数据库的页面大小只是作为备份操作的一部分而更改的。唯一的例外是,如果目标数据库恰好是内存数据库。在这种情况下,如果在备份操作开始时页面大小不同,则操作将失败并返回 SQLITE_READONLY 错误。不幸的是,这可能会在使用函数 loadOrSaveDb() 将数据库映像从文件加载到内存数据库时发生。

但是,如果内存数据库 pInMemory 刚被打开(因此完全为空)然后再传递给函数 loadOrSaveDb(),那么仍然可以使用 SQLite “PRAGMA page_size” 命令更改其页面大小。函数 loadOrSaveDb() 可以检测到这种情况,并尝试在调用在线备份 API 函数之前将内存数据库的页面大小设置为数据库 zFilename 的页面大小。

示例 2:正在运行数据库的在线备份

/*
** Perform an online backup of database pDb to the database file named
** by zFilename. This function copies 5 database pages from pDb to
** zFilename, then unlocks pDb and sleeps for 250 ms, then repeats the
** process until the entire database is backed up.
** 
** The third argument passed to this function must be a pointer to a progress
** function. After each set of 5 pages is backed up, the progress function
** is invoked with two integer parameters: the number of pages left to
** copy, and the total number of pages in the source file. This information
** may be used, for example, to update a GUI progress bar.
**
** While this function is running, another thread may use the database pDb, or
** another process may access the underlying database file via a separate 
** connection.
**
** If the backup process is successfully completed, SQLITE_OK is returned.
** Otherwise, if an error occurs, an SQLite error code is returned.
*/
int backupDb(
  sqlite3 *pDb,               /* Database to back up */
  const char *zFilename,      /* Name of file to back up to */
  void(*xProgress)(int, int)  /* Progress function to invoke */     
){
  int rc;                     /* Function return code */
  sqlite3 *pFile;             /* Database connection opened on zFilename */
  sqlite3_backup *pBackup;    /* Backup handle used to copy data */

  /* Open the database file identified by zFilename. */
  rc = sqlite3_open(zFilename, &pFile);
  if( rc==SQLITE_OK ){

    /* Open the sqlite3_backup object used to accomplish the transfer */
    pBackup = sqlite3_backup_init(pFile, "main", pDb, "main");
    if( pBackup ){

      /* Each iteration of this loop copies 5 database pages from database
      ** pDb to the backup database. If the return value of backup_step()
      ** indicates that there are still further pages to copy, sleep for
      ** 250 ms before repeating. */
      do {
        rc = sqlite3_backup_step(pBackup, 5);
        xProgress(
            sqlite3_backup_remaining(pBackup),
            sqlite3_backup_pagecount(pBackup)
        );
        if( rc==SQLITE_OK || rc==SQLITE_BUSY || rc==SQLITE_LOCKED ){
          sqlite3_sleep(250);
        }
      } while( rc==SQLITE_OK || rc==SQLITE_BUSY || rc==SQLITE_LOCKED );

      /* Release resources allocated by backup_init(). */
      (void)sqlite3_backup_finish(pBackup);
    }
    rc = sqlite3_errcode(pFile);
  }
  
  /* Close the database connection opened on database file zFilename
  ** and return the result of this function. */
  (void)sqlite3_close(pFile);
  return rc;
}

上一示例中介绍的函数通过一次调用 sqlite3_backup_step() 来复制整个源数据库。这需要在整个操作过程中对源数据库文件保持读锁,从而阻止任何其他数据库用户写入数据库。它还始终持有与数据库 pInMemory 关联的互斥锁,阻止任何其他线程使用它。本节中的 C 函数旨在由后台线程或进程调用以创建在线数据库的备份,它使用以下方法避免了这些问题

  1. 调用函数 sqlite3_backup_init() 来创建一个 sqlite3_backup 对象,用于将数据从数据库 pDb 复制到由 zFilename 标识的备份数据库文件。
  2. 调用函数 sqlite3_backup_step(),参数为 5,以将数据库 pDb 的 5 页复制到备份数据库(文件 zFilename)。
  3. 如果数据库 pDb 中还有更多页要复制,则该函数休眠 250 毫秒(使用 sqlite3_sleep() 实用程序),然后返回到步骤 2。
  4. 调用函数 sqlite3_backup_finish() 来清理 sqlite3_backup_init() 分配的资源。

文件和数据库连接锁定

在上面的步骤 3 中的 250 毫秒休眠期间,不会在数据库文件上保持读锁,并且不会持有与 pDb 关联的互斥锁。这允许其他线程使用 数据库连接 pDb 和其他连接写入底层数据库文件。

如果另一个线程或进程在该函数休眠时写入源数据库,则 SQLite 会检测到这一点,并且通常会在下次调用 sqlite3_backup_step() 时重新启动备份过程。该规则有一个例外:如果源数据库不是内存数据库,并且写入是在与备份操作相同的进程中执行的,并且使用相同的数据库句柄 (pDb),则目标数据库(使用连接 pFile 打开的数据库)会与源数据库一起自动更新。备份过程可能会在 sqlite3_sleep() 调用返回后继续,就好像什么也没发生一样。

无论备份过程是否由于在备份过程中写入源数据库而重新启动,用户都可以确定当备份操作完成后,备份数据库包含源数据库的完整一致的快照。但是

backup_remaining() 和 backup_pagecount()

backupDb() 函数使用 sqlite3_backup_remaining() 和 sqlite3_backup_pagecount() 函数通过用户提供的 xProgress() 回调报告其进度。函数 sqlite3_backup_remaining() 返回要复制的页数,而 sqlite3_backup_pagecount() 返回源数据库中的总页数(在本例中为由 pDb 打开的数据库)。因此,过程的完成百分比可以计算为

完成度 = 100% * (pagecount() - remaining()) / pagecount()

sqlite3_backup_remaining() 和 sqlite3_backup_pagecount() API 报告由前一次调用 sqlite3_backup_step() 存储的值,它们实际上不会检查源数据库文件。这意味着,如果在调用 sqlite3_backup_step() 返回后,但在使用 sqlite3_backup_remaining() 和 sqlite3_backup_pagecount() 返回的值之前,另一个线程或进程写入源数据库,则这些值在技术上可能是错误的。这通常不是问题。