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

使用 sqlite3_unlock_notify() API

/* This example uses the pthreads API */
#include <pthread.h>

/*
** A pointer to an instance of this structure is passed as the user-context
** pointer when registering for an unlock-notify callback.
*/
typedef struct UnlockNotification UnlockNotification;
struct UnlockNotification {
  int fired;                         /* True after unlock event has occurred */
  pthread_cond_t cond;               /* Condition variable to wait on */
  pthread_mutex_t mutex;             /* Mutex to protect structure */
};

/*
** This function is an unlock-notify callback registered with SQLite.
*/
static void unlock_notify_cb(void **apArg, int nArg){
  int i;
  for(i=0; i<nArg; i++){
    UnlockNotification *p = (UnlockNotification *)apArg[i];
    pthread_mutex_lock(&p->mutex);
    p->fired = 1;
    pthread_cond_signal(&p->cond);
    pthread_mutex_unlock(&p->mutex);
  }
}

/*
** This function assumes that an SQLite API call (either sqlite3_prepare_v2() 
** or sqlite3_step()) has just returned SQLITE_LOCKED. The argument is the
** associated database connection.
**
** This function calls sqlite3_unlock_notify() to register for an 
** unlock-notify callback, then blocks until that callback is delivered 
** and returns SQLITE_OK. The caller should then retry the failed operation.
**
** Or, if sqlite3_unlock_notify() indicates that to block would deadlock 
** the system, then this function returns SQLITE_LOCKED immediately. In 
** this case the caller should not retry the operation and should roll 
** back the current transaction (if any).
*/
static int wait_for_unlock_notify(sqlite3 *db){
  int rc;
  UnlockNotification un;

  /* Initialize the UnlockNotification structure. */
  un.fired = 0;
  pthread_mutex_init(&un.mutex, 0);
  pthread_cond_init(&un.cond, 0);

  /* Register for an unlock-notify callback. */
  rc = sqlite3_unlock_notify(db, unlock_notify_cb, (void *)&un);
  assert( rc==SQLITE_LOCKED || rc==SQLITE_OK );

  /* The call to sqlite3_unlock_notify() always returns either SQLITE_LOCKED 
  ** or SQLITE_OK. 
  **
  ** If SQLITE_LOCKED was returned, then the system is deadlocked. In this
  ** case this function needs to return SQLITE_LOCKED to the caller so 
  ** that the current transaction can be rolled back. Otherwise, block
  ** until the unlock-notify callback is invoked, then return SQLITE_OK.
  */
  if( rc==SQLITE_OK ){
    pthread_mutex_lock(&un.mutex);
    if( !un.fired ){
      pthread_cond_wait(&un.cond, &un.mutex);
    }
    pthread_mutex_unlock(&un.mutex);
  }

  /* Destroy the mutex and condition variables. */
  pthread_cond_destroy(&un.cond);
  pthread_mutex_destroy(&un.mutex);

  return rc;
}

/*
** This function is a wrapper around the SQLite function sqlite3_step().
** It functions in the same way as step(), except that if a required
** shared-cache lock cannot be obtained, this function may block waiting for
** the lock to become available. In this scenario the normal API step()
** function always returns SQLITE_LOCKED.
**
** If this function returns SQLITE_LOCKED, the caller should rollback
** the current transaction (if any) and try again later. Otherwise, the
** system may become deadlocked.
*/
int sqlite3_blocking_step(sqlite3_stmt *pStmt){
  int rc;
  while( SQLITE_LOCKED==(rc = sqlite3_step(pStmt)) ){
    rc = wait_for_unlock_notify(sqlite3_db_handle(pStmt));
    if( rc!=SQLITE_OK ) break;
    sqlite3_reset(pStmt);
  }
  return rc;
}

/*
** This function is a wrapper around the SQLite function sqlite3_prepare_v2().
** It functions in the same way as prepare_v2(), except that if a required
** shared-cache lock cannot be obtained, this function may block waiting for
** the lock to become available. In this scenario the normal API prepare_v2()
** function always returns SQLITE_LOCKED.
**
** If this function returns SQLITE_LOCKED, the caller should rollback
** the current transaction (if any) and try again later. Otherwise, the
** system may become deadlocked.
*/
int sqlite3_blocking_prepare_v2(
  sqlite3 *db,              /* Database handle. */
  const char *zSql,         /* UTF-8 encoded SQL statement. */
  int nSql,                 /* Length of zSql in bytes. */
  sqlite3_stmt **ppStmt,    /* OUT: A pointer to the prepared statement */
  const char **pz           /* OUT: End of parsed string */
){
  int rc;
  while( SQLITE_LOCKED==(rc = sqlite3_prepare_v2(db, zSql, nSql, ppStmt, pz)) ){
    rc = wait_for_unlock_notify(db);
    if( rc!=SQLITE_OK ) break;
  }
  return rc;
}

當兩個或多個連線以共享快取模式存取同一個資料庫時,會使用個別資料表的讀取和寫入 (共享和獨佔) 鎖定,以確保同時執行的交易保持孤立。在寫入資料表之前,必須取得該資料表的寫入 (獨佔) 鎖定。在讀取之前,必須取得讀取 (共享) 鎖定。連線會在結束交易時釋放所有已持有的資料表鎖定。如果連線無法取得所需的鎖定,則呼叫 sqlite3_step() 會傳回 SQLITE_LOCKED。

雖然較不常見,但呼叫 sqlite3_prepare()sqlite3_prepare_v2() 也有可能傳回 SQLITE_LOCKED,如果它無法取得每個附加資料庫的 sqlite_schema 資料表 的讀取鎖定。這些 API 需要讀取 sqlite_schema 資料表中包含的架構資料,才能將 SQL 陳述式編譯成 sqlite3_stmt* 物件。

本文介紹一種使用 SQLite sqlite3_unlock_notify() 介面的技術,讓呼叫 sqlite3_step()sqlite3_prepare_v2() 會阻擋,直到所需的鎖定可用,而不是立即傳回 SQLITE_LOCKED。如果左側顯示的 sqlite3_blocking_step() 或 sqlite3_blocking_prepare_v2() 函數傳回 SQLITE_LOCKED,這表示阻擋會讓系統陷入僵局。

僅當使用定義了前置處理器符號 SQLITE_ENABLE_UNLOCK_NOTIFY 編譯函式庫時,sqlite3_unlock_notify() API 才可用,在此處記錄。本文並非用於取代閱讀完整的 API 文件!

sqlite3_unlock_notify() 介面設計用於在系統中,為每個 資料庫連線 指定一個獨立執行緒。實作中並無任何內容可防止單一執行緒執行多個資料庫連線。然而,sqlite3_unlock_notify() 介面一次僅作用於單一連線,因此,在此顯示的鎖定解析邏輯僅對每個執行緒的單一資料庫連線有效。

sqlite3_unlock_notify() API

在呼叫 sqlite3_step()sqlite3_prepare_v2() 後傳回 SQLITE_LOCKED,可以呼叫 sqlite3_unlock_notify() API 來註冊解鎖通知回呼。資料庫連線持有阻止呼叫 sqlite3_step()sqlite3_prepare_v2() 成功進行的表格鎖定後,SQLite 會呼叫解鎖通知回呼,並釋放所有鎖定。例如,如果呼叫 sqlite3_step() 是嘗試從表格 X 讀取,而其他連線 Y 持有表格 X 的寫入鎖定,則 sqlite3_step() 會傳回 SQLITE_LOCKED。如果接著呼叫 sqlite3_unlock_notify(),則在連線 Y 的交易結束後,會呼叫解鎖通知回呼。解鎖通知回呼正在等待的連線(在本例中為連線 Y)稱為「封鎖連線」。

如果呼叫 sqlite3_step() 嘗試寫入資料庫表格並傳回 SQLITE_LOCKED,則可能有多個其他連線持有對問題資料庫表格的讀取鎖定。在這種情況下,SQLite 會任意選取其中一個其他連線,並在該連線的交易結束時發出解鎖通知回呼。無論呼叫 sqlite3_step() 是被一個或多個連線封鎖,當發出對應的解鎖通知回呼時,並不保證所需的鎖定可用,只保證它可能可用。

發出解鎖通知回呼時,會從與封鎖連線相關聯的 sqlite3_step()(或 sqlite3_close())呼叫中發出。從解鎖通知回呼中呼叫任何 sqlite3_XXX() API 函數都是非法的。預期的用途是解鎖通知回呼會對其他等待執行緒發出訊號,或排程稍後執行的動作。

sqlite3_blocking_step() 函數使用的演算法如下

  1. 對提供的陳述句控制代碼呼叫 sqlite3_step()。如果呼叫傳回除 SQLITE_LOCKED 以外的任何值,則將此值傳回給呼叫者。否則,繼續。

  2. 針對提供的陳述句處理,呼叫 sqlite3_unlock_notify() 註冊解除鎖定通知的回呼。如果呼叫 unlock_notify() 傳回 SQLITE_LOCKED,則將此值傳回給呼叫者。

  3. 封鎖,直到其他執行緒呼叫解除鎖定通知的回呼。

  4. 針對陳述句處理呼叫 sqlite3_reset()。由於 SQLITE_LOCKED 錯誤可能只會發生在第一次呼叫 sqlite3_step() 時(無法讓一次呼叫 sqlite3_step() 傳回 SQLITE_ROW,然後下一次傳回 SQLITE_LOCKED),因此陳述句處理可以在此點重設,而不會影響呼叫者觀點中查詢的結果。如果在此點未呼叫 sqlite3_reset(),則下一次呼叫 sqlite3_step() 將傳回 SQLITE_MISUSE。

  5. 回到步驟 1。

sqlite3_blocking_prepare_v2() 函數使用的演算法類似,但省略了步驟 4(重設陳述句處理)。

寫入者飢餓

多個連線可以同時持有讀取鎖定。如果許多執行緒取得重疊的讀取鎖定,則可能至少有一個執行緒始終持有讀取鎖定。然後等待寫入鎖定的表格將永遠等待。此情況稱為「寫入者飢餓」。

SQLite 可協助應用程式避免寫入者飢餓。在嘗試取得表格的寫入鎖定失敗後(因為一個或多個其他連線持有讀取鎖定),所有嘗試在共用快取上開啟新交易的動作都會失敗,直到符合下列其中一個條件為止

開啟新讀取交易失敗會傳回 SQLITE_LOCKED 給呼叫者。如果呼叫者接著呼叫 sqlite3_unlock_notify() 來註冊解鎖通知回呼,則封鎖連線是目前在共用快取上開啟寫入交易的連線。這可以防止寫入器挨餓,因為如果沒有新的讀取交易可以開啟,並且假設所有現有的讀取交易最終都會結束,那麼寫入器最終將有機會取得所需的寫入鎖定。

pthreads API

當 wait_for_unlock_notify() 呼叫 sqlite3_unlock_notify() 時,防止 sqlite3_step() 或 sqlite3_prepare_v2() 呼叫成功的封鎖連線可能已經完成其交易。在這種情況下,解鎖通知回呼會在 sqlite3_unlock_notify() 傳回之前立即呼叫。或者,解鎖通知回呼可能在呼叫 sqlite3_unlock_notify() 之後但在執行緒開始等待非同步訊號之前由第二個執行緒呼叫。

這種潛在競爭條件的確切處理方式取決於應用程式使用的執行緒和同步原語介面。此範例使用 pthreads,這是由 Linux 等現代類 UNIX 系統提供的介面。

pthreads 介面提供 pthread_cond_wait() 函式。此函式允許呼叫者同時釋放互斥鎖並開始等待非同步訊號。使用此函式,「觸發」旗標和互斥鎖,可以消除上述競爭條件,如下所示

當呼叫 sqlite3_unlock_notify() 的執行緒開始等待非同步訊號之前,可能會呼叫解鎖通知回呼,它會執行下列動作

  1. 取得互斥鎖。
  2. 將「fired」旗標設為 true。
  3. 嘗試發出訊號給等待中的執行緒。
  4. 釋放互斥鎖。

當 wait_for_unlock_notify() 執行緒準備開始等待解鎖通知回呼時,它會

  1. 取得互斥鎖。
  2. 檢查「fired」旗標是否已設定。如果是,則解鎖通知回呼已呼叫。釋放互斥鎖並繼續。
  3. 原子性地釋放互斥鎖並開始等待非同步訊號。當訊號到達時,繼續。

這樣一來,當 wait_for_unlock_notify() 執行緒開始封鎖時,解鎖通知回呼是否已呼叫或正在呼叫並不重要。

可能的改善

本文中的程式碼至少可以用兩種方式改善

儘管 sqlite3_unlock_notify() 函式只允許呼叫者指定單一使用者內容指標,但解鎖通知回呼會傳遞此類內容指標的陣列。這是因為當封鎖連線結束其交易時,如果有超過一個解鎖通知註冊呼叫相同的 C 函式,內容指標會封送至陣列中,並發出單一回呼。如果每個執行緒都被指定一個優先順序,則這個實作不會只是以任意順序發出訊號給執行緒,而是可以先發出訊號給優先順序較高的執行緒,再發出訊號給優先順序較低的執行緒。

如果執行「DROP TABLE」或「DROP INDEX」SQL 指令,且同一個資料庫連線目前有一個或多個正在執行的 SELECT 陳述式,則會傳回 SQLITE_LOCKED。如果在此情況下呼叫 sqlite3_unlock_notify(),則會立即呼叫指定的回呼。重新嘗試「DROP TABLE」或「DROP INDEX」陳述式會傳回另一個 SQLITE_LOCKED 錯誤。在左側顯示的 sqlite3_blocking_step() 實作中,這可能會導致無限迴圈。

呼叫者可以使用 延伸錯誤碼 來區分這個特殊的「DROP TABLE|INDEX」案例和其它案例。當適當呼叫 sqlite3_unlock_notify() 時,延伸錯誤碼為 SQLITE_LOCKED_SHAREDCACHE。否則,在「DROP TABLE|INDEX」案例中,它只是單純的 SQLITE_LOCKED。另一個解決方案可能是限制任何單一查詢可以重新嘗試的次數(例如 100 次)。雖然這可能比預期的效率低,但所討論的情況不太可能經常發生。

此頁面最後修改於 2022-01-08 05:02:57 UTC