历史上,SQLite数据库的备份(副本)是使用以下方法创建的:
此过程在许多情况下都可以正常运行,并且通常非常快。但是,该技术具有以下缺点:
该在线备份API是为了解决这些问题。在线备份API允许将一个数据库的内容复制到另一个数据库中,从而覆盖目标数据库的原始内容。复制操作可以增量执行,在这种情况下,在复制期间不需要锁定源数据库,而仅在实际读取源数据库的短暂时间段内才需要锁定源数据库。这样,在进行联机数据库备份时,其他数据库用户可以继续不间断地运行。
在线备份API记录在这里。该页面的其余部分包含两个C语言示例,这些示例说明了API的常见用法及其讨论。阅读这些示例不能代替阅读API文档!
更新:SQLite版本3.27.0(2019-02-07)中引入的VACUUM INTO命令可以替代备份API。
/ * **此函数用于将磁盘 ** 上的数据库文件的内容加载到打开的数据库连接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的用法如下:
错误处理
如果这三个主要备份API例程中的任何一个发生错误,则错误代码和消息将附加到目标数据库连接。此外,如果 sqlite3_backup_step()遇到一个错误,则该错误码是由两个返回sqlite3_backup_step()调用自身通过到后续呼叫,并sqlite3_backup_finish() 。因此,调用sqlite3_backup_finish() 不会覆盖由sqlite3_backup_step()存储在目标数据库连接中的错误代码。 。示例代码中使用了此功能,以减少所需的错误处理量。sqlite3_backup_step()和sqlite3_backup_finish() 调用的返回值将被忽略,并且 此后指示从目标数据库连接收集的复制操作成功或失败的错误代码。
可能的增强
可以通过至少两种方式来增强此功能的实现:
由于数据库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的页面大小。
/ * **将数据库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函数旨在由后台线程或创建联机数据库备份的进程调用,它使用以下方法避免了这些问题:
文件和数据库连接锁定
在上述步骤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()返回的值之前,源数据库是由另一个线程或进程写入的,则这些值在技术上可能是错误的。这通常不是问题。