小巧、快速、可靠。
任選三項。

使用 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() 傳回的值之前,原始資料庫被另一個執行緒或程序寫入,則這些值在技術上可能不正確。這通常不是問題。