Small. Fast. Reliable.
Choose any three.

使用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:加载和保存内存数据库

/ * 
**此函数用于将磁盘
**
上的数据库文件的内容加载到打开的数据库连接pInMemory的“主”数据库中,或**
将由pInMemory打开的数据库的当前内容保存到** a中磁盘上的数据库文件。pInMemory可能是内存数据库,
**,但如果不是
此功能也可以正常工作。**
参数zFilename指向一个以nul结尾的字符串,该字符串包含
要从中加载或保存到的磁盘上数据库文件
**名称。如果参数** isSave不为零,则文件zFilename
的内容将被pInMemory打开的数据库的内容**覆盖。如果
**参数isSave为零,则通过以下方式打开数据库的内容
** pInMemory被替换为从文件zFilename加载的数据。
** 
**如果操作成功,则返回SQLITE_OK。否则,如果
**发生错误,则返回SQLite错误代码。
* / 
int loadOrSaveDb(sqlite3 * pInMemory,const char * zFilename,int isSave){
  int rc;                   / *函数返回码* / 
  sqlite3 * pFile;           / *在zFilename上打开数据库连接* / 
  sqlite3_backup * pBackup;  / *用于复制数据的备份对象* / 
  sqlite3 * pTo;             / *要复制到(pFile或pInMemory)的数据库* / 
  sqlite3 * pFrom;           / *要从(pFile或pInMemory)复制的数据库* /

  / *打开由zFilename标识的数据库文件。如果
  由于任何原因而失败**,请提早退出* / 
  rc = sqlite3_open(zFilename,&pFile);
  if(rc == SQLITE_OK){

    / *如果这是一个“加载”操作(isSave == 0),则将数据
    从刚刚打开的数据库文件中
    复制**到数据库pInMemory。**否则,如果这是一个“保存”操作(isSave == 1),则将数据
    **从pInMemory复制到pFile。相应地设置变量pFrom和
    ** pTo。* /
    pFrom =(isSave?pInMemory:pFile);
    pTo =(isSave?pFile:pInMemory);

    / *设置备份过程,以从
    **连接pFile
    的“主”数据库复制到连接pInMemory的主数据库。**如果出了问题,pBackup将设置为NULL,
    并且连接pTo中将保留
    错误**代码和消息。** 
    **如果成功创建了备份对象,请调用backup_step()
    **将数据从pFile复制到pInMemory。然后调用backup_finish()
    **释放与pBackup对象关联的资源。如果发生
    **错误,则错误代码和消息将保留在
    **连接pTo中。如果未发生错误,则将属于
    pTo的**
    的错误代码设置为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);
  }

  / *关闭在数据库文件zFilename 
  **上打开的数据库连接,并返回此函数的结果。* / 
  (void)sqlite3_close(pFile);
  返回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。解决方案 是打开后立即使用sqlite3_busy_handler()sqlite3_busy_timeout()数据库连接pFile注册一个繁忙处理程序回调或超时。如果不能立即获得所需的锁, sqlite3_backup_step()使用相同的方式为所有已注册的忙处理程序回调或超时sqlite3_step()sqlite3_exec()可以。

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

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

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

/ * 
**将数据库pDb联机备份到zFilename命名为
**
的数据库文件此函数将5个数据库页面从pDb复制到** zFilename,然后解锁pDb并休眠250 ms,然后重复
**过程,直到备份了整个数据库。
**
传递给此函数的第三个参数必须是指向progress
函数
的指针备份每组5页后,将使用两个整数参数调用
进度函数**:保留**复制的页数以及源文件中的总页数。
例如,
此信息**可用于更新GUI进度栏。**
**在运行此功能时,另一个线程可能会使用数据库pDb,或者
**另一个进程可能会通过单独的
**连接
访问基础数据库文件** 
**如果备份过程成功完成,则返回SQLITE_OK。
**否则,如果发生错误,将返回SQLite错误代码。
* /
int backupDb(
  sqlite3 * pDb,                / *要备份的数据库* / 
  const char * zFilename,       / *要备份到* /的文件名*( 
  void(* xProgress)(int,int)   / *要调用的进度函数* /     
){
  int rc;                     / *函数返回码* / 
  sqlite3 * pFile;             / *在zFilename上打开数据库连接* / 
  sqlite3_backup * pBackup;    / *用于复制数据的备份句柄* /

  / *打开由zFilename标识的数据库文件。* / 
  rc = sqlite3_open(zFilename,&pFile);
  if(rc == SQLITE_OK){

    / *打开用于完成传输的sqlite3_backup对象* / 
    pBackup = sqlite3_backup_init(pFile,“ main”,pDb,“ main”);
    if(pBackup){

      / *此循环的每次迭代都将5个数据库页面从数据库
      ** pDb复制到备份数据库。如果backup_step()
      **
      的返回值表明还有其他页面要复制,请在** 250 ms内睡眠,然后再重复。* /
      做 {
        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);

      / *释放由backup_init()分配的资源。* / 
      (无效)sqlite3_backup_finish(pBackup);
    }
    rc = sqlite3_errcode(pFile);
  }
  
  / *关闭在数据库文件zFilename 
  **上打开的数据库连接,并返回此函数的结果。* / 
  (void)sqlite3_close(pFile);
  返回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()调用返回之后,就可以继续执行备份过程,就好像什么都没有发生一样。

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

backup_remaining()和backup_pagecount()

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

完成= 100%*(pagecount()-剩余())/ pagecount()

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