使用 SQLite 在线备份 API

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

  1. 使用SQLite API(即shell工具)在数据库文件上建立共享锁。
  2. 使用外部工具(例如 unix“cp”实用程序或 DOS“复制”命令)复制数据库文件。
  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. 使用参数-1调用 函数sqlite3_backup_step()以将整个源数据库复制到目标。
  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. 使用参数 5 调用函数sqlite3_backup_step()将数据库 pDb 的 5 页复制到备份数据库(文件 zFilename)。
  3. 如果还有更多页面要从数据库 pDb 复制,那么该函数将休眠 250 毫秒(使用sqlite3_sleep() 实用程序),然后返回到步骤 2。
  4. 调用函数sqlite3_backup_finish()来清理由sqlite3_backup_init()分配的资源。

文件和数据库连接锁定

在上面第 3 步中的 250 ms 睡眠期间,数据库文件上没有读取锁,并且与 pDb 关联的互斥锁也没有被持有。这允许其他线程使用数据库连接pDb 和其他连接来写入底层数据库文件。

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

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

  • 写入内存源数据库,或通过外部进程或线程使用 pDb 以外的数据库连接写入基于文件的源数据库,比使用 pDb 写入基于文件的源数据库的成本要高得多(因为前两种情况必须重新启动整个备份操作)。
  • 如果备份过程重新启动足够频繁,它可能永远不会运行完成并且 backupDb() 函数可能永远不会返回。

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() 返回的值之前,源数据库被另一个线程或进程写入,这些值在技术上可能是不正确的。这通常不是问题。