最近項目中涉及到 sqlite 並發讀寫的問題,最終發現基線兩個數據庫使用同一個 db_connect() 接口,都存在並發訪問沖突隱患,但只在 H11 平台上出現。是因為其它平台性能好,“只要你 CPU 執行速度夠快,我 dhcp 就能完美錯開 sipServer 初始化,避免沖突” 。
參考一些文檔並結合自己的實踐,對 sqlite3 並發問題總結了幾點:
sqlite3 的鎖及事務類型
sqlite3 總共有三種事務類型:BEGIN [DEFERRED /IMMEDIATE / EXCLUSIVE] TRANSCATION,五種鎖,按鎖的級別依次是:UNLOCKED /SHARED /RESERVERD /PENDING /EXCLUSIVE。當執行 select 即讀操作時,需要獲取到 SHARED 鎖(共享鎖),當執行 insert/update/delete 操作 (即內存寫操作時),需要進一步獲取到 RESERVERD 鎖(保留鎖),當進行 commit 操作 (即磁盤寫操作時),需要進一步獲取到 EXCLUSIVE 鎖(排它鎖)。
對於 RESERVERD 鎖,sqlite3 保證同一時間只有一個連接可以獲取到保留鎖,也就是同一時間只有一個連接可以寫數據庫 (內存),但是其它連接仍然可以獲取 SHARED 鎖,也就是其它連接仍然可以進行讀操作(這里可以認為寫操作只是對磁盤數據的一份內存拷貝進行修改,並不影響讀操作)。
對於 EXCLUSIVE 鎖,是比保留鎖更為嚴格的一種鎖,在需要把修改寫入磁盤即 commit 時需要在保留鎖 / 未決鎖的基礎上進一步獲取到排他鎖,顧名思義,排他鎖排斥任何其它類型的鎖,即使是 SHARED 鎖也不行,所以,在一個連接進行 commit 時,其它連接是不能做任何操作的(包括讀)。
PENDING 鎖(即未決鎖),則是比較特殊的一種鎖,它可以允許已獲取到 SHARED 鎖的事務繼續進行,但不允許其它連接再獲取 SHARED 鎖,當已存在的 SHARED 鎖都被釋放后(事務執行完成),持有未決鎖的事務就可以獲得 commit 的機會了。sqlite3 使用這種鎖來防止 writer starvation(寫餓死)。
死鎖的情況
死鎖的情況:當兩個連接使用 begin transaction 開始事務時,第一個連接執行了一次 select 操作(已經獲取到 SHARED 鎖),第二個連接執行了一次 insert 操作(已經獲取到了 RESERVERD 鎖),此時第一個連接需要進行一次 insert/update/delete(需要獲取到 RESERVERD 鎖),第二個連接則希望執行 commit(需要獲取到 EXCLUSIVE 鎖),由於第二個連接已經獲取到了 RESERVERD 鎖,根據 RESERVERD 鎖同一時間只有一個連接可以獲取的特性,第一個連接獲取 RESERVERD 鎖的操作必定失敗,而由於第一個連接已經獲取到 SHARED 鎖,第二個連接希望進一步獲取到 EXCLUSIVE 鎖的操作也必定失敗。就導致了事務死鎖。
事務類型的使用原則
在用”begin transaction” 顯式開啟一個事務時,默認的事務類型為 DEFERRED,鎖的狀態為 UNLOCKED,即不獲取任何鎖,如果在使用的數據庫沒有其它的連接,用 begin 就可以了。如果有多個連接都需要對數據庫進行寫操作,那就得使用 BEGIN IMMEDIATE/EXCLUSIVE 開始事務了。
使用事務的好處是:1. 一個事務的所有操作相當於一次原子操作,如果其中某一步失敗,可以通過回滾來撤銷之前所有的操作,只有當所有操作都成功時,才進行 commit,保證了操作的原子特性;2. 對於多次的數據庫操作,如果我們希望提高數據查詢或更新的速度,可以在開始操作前顯式開啟一個事務,在執行完所有操作后,再通過一次 commit 來提交所有的修改或結束事務。
對 SQLITE_BUSY 的處理
當有多個連接同時對數據庫進行寫操作時,根據事務類型的使用原則,我們在每個連接中用 BEGIN IMMEDIATE 開始事務,即多個連接都嘗試取得保留鎖的情況,根據保留鎖同一時間只有一個連接可以獲取到的特性,其它連接都將獲取失敗,即事務開始失敗,這種情況下,sqlite3 將返回一個 SQLITE_BUSY 的錯誤,如果我們不希望操作就此失敗而返回,就必須處理 SQLITE_BUSY 的情況,sqlite3 提供了 sqlite3_busy_handler 或 sqlite3_busy_timeout 來處理 SQLITE_BUSY,對於 sqlite3_busy_handler,我們可以指定一個 busy_handler 來處理,並可以指定失敗重試的次數。而 sqlite3_busy_timeout 則是由 sqlite3 自動進行 sleep 並重試,當 sleep 的累積時間超過指定的超時時間時,最終返回 SQLITE_BUSY。需要注意的是,這兩個函數同時只能使用一個,后面的調用會覆蓋掉前次調用。從使用上來說,sqlite3_busy_timeout 更易用一些,只需要指定一個總的超時時間,然后 sqlite 自己會決定多久進行重試以及重試的次數,直到達到總的超時時間最終返回 SQLITE_BUSY。並且,這兩個函數一經調用,對其后的所有數據庫操作都有效,非常方便。
解決方法:
綜上,我們不難發現並發讀寫的時候出現了事務死鎖,最終解決方法如下:
法一:信號量實現互斥
sem_p(semid, 0); sqlite3_exec(db, buf, 0, 0, &pErrMsg); sem_v(semid, 0);
法二:自定義循環訪問
do { ret = sqlite3_exec(db, buf, 0, 0, &pErrMsg); if (ret == SQLITE_BUSY) { sleep(1); continue; } break; }while(1);
法三:使用 sqlite3 的 API,當檢測到當前連接的數據庫處於 SQLITE_BUSY 時等待,或自定義 busy 時的回調處理
