深入SQLite,一網打盡“危險操作”

Jerry_Tang發表於2018-07-05

簡述

Sqlite Database是一個比較穩定輕量的小型資料庫,不像是mysql、oracle等資料庫一樣具有單獨的服務程式。基於sqlite的讀寫都是讀寫原始的磁碟檔案,也就是說sqlite的操作完全是磁碟的IO操作。SQLite操作存在一定的異常情況,在客戶端也難以監控,在使用者基數比較大的情況下,往往對極少數的使用者的影響也是致命的。瞭解sqlite便於我們規範的去使用它。

通常的異常情況都包含在以下情況:

  • 檔案錯寫
  • 檔案鎖 bug
  • 檔案 sync 失敗
  • 裝置損壞
  • 記憶體覆蓋
  • 作業系統 bug
  • SQLite bug

官方的文件也有介紹到

How To Corrupt An SQLite Database File

鑑於作業系統和Sqlite bug等不可控方面我們更多需要做到能準確的監控,如記憶體不足,類似於微信的做法就是監控到記憶體不足,彈出提示框告知使用者去清理記憶體。下面就來探討下由於操作不當引起的異常。

SQLiteDatabaseLockedException

android.database.sqlite.SQLiteDatabaseLockedException DB引擎在執行job的時候發現獲取不到資料庫的鎖就會丟擲這個異常。資料庫鎖用於防止多執行緒同時寫入資料從而導致DB損壞。所以一般發生在多執行緒的事務操作上。針對同一個事務程式碼段,如果一個事務尚未結束提交,另外一個執行緒便開始介入執行就會導致衝突丟擲此異常。

所以出現上述異常,請檢查你的程式碼。建議的操作是:

  1. 只使用一個SQLOpenHelper來訪問和操作database,單例你的SQLOpenHelper,避免多個OpenHelper多次open db並寫入資料。
  2. 當不再需要繼續操作db的時候,關閉所有database helper例項
  3. 做事務操作的時候一定要保證事務完成呼叫了endTransaction() 或者transaction Successful相關方法。下一個事務操作會在entransaction結束後請求鎖。

檔案鎖

系統檔案鎖問題SQLite依賴於底層的檔案系統對檔案鎖的實現。SQLite 預設鎖是協同鎖。假設有A、B執行緒同時訪問資料庫並寫入資料。這個時候來個C執行緒不是使用SQLite API的方式來運算元據的話(如直接的IO操作),那麼資料庫鎖就會被自動取消,A、B執行緒在沒有鎖保護的狀態同時操作db就可能導致DB的損壞。

建議:

  1. 儘量使用SQLite API來運算元據庫的讀寫。
  2. 如果有IO執行緒直接運算元據庫,程式碼邏輯上保證執行緒間的同步順序。

檔案sync失敗

批量寫入比較大的資料我們可能想要更快的速度,網上很多會建議你設定PRAGMA synchronous=OFF, Android對應的設定方式就是:mDatabase.execSQL("PRAGMA synchronous=OFF"); 等同於在說:我期待更快的寫入速度,磁碟驅動器為了達到目標,就耍了個小聰明,忽略開始SQLite的同步操作,先通知到系統我寫入完了。實際上不一定資料完全寫入了,假如這個時候出現了斷電等意外,就會導致真實資料寫入失敗從而引發DB損壞。

建議:

儘量不要設定PRAGMA synchronous=OFF 資料比較大的寫入等操作從sql語句優化和效能的優化入手。

多db操作

很多較為複雜的應用可能一個應用程式不止有一個資料庫。這裡列舉一個場景,應用包含兩個DB。 DatabaseA:預置資料的資料庫,已經設計好表並提前插入了資料。不需要跟隨應用動態的去建立DB。使用者第一次安裝的時候把準備好的DB通過IO的方式拷貝到應用包路徑下面的databases路徑提供給應用訪問,此DB一般只用讀,很少或者完全不寫入資料,是靜態的。 DatabaseB: 跟隨應用的安裝和升級動態的建立和升級,作為應用資料的持久化方式之一。讀寫操作均比較頻繁。

DababaseA會在應用的啟動的時候IO遷移拷貝。DatabaseB動態Create後到處可能都有使用到(包括啟動)

Danger 1:檔案覆蓋

DatabaseB的DBOpenHelperB正在讀寫DatabaseA的表,同時DatabaseA正在被IO執行緒拷貝寫入檔案覆蓋。DBOpenHelperB建立連線可能依然是舊的資料庫,讀寫自然也是到舊的資料庫。部分檔案系統甚至引發IO拷貝被中斷。

Danger 2:交叉讀寫

一般有多個DB會對應多個DBOpenHelper進行讀寫,如過DBOpenHelper做了跨DB的讀寫,就會導致重複的DB Open或Close,影響到了其他DBOpenHelper的正常讀寫。直接的導致鎖異常或者DB損壞等問題。

多DB建議:

  1. 非SQLite API執行緒IO資料庫請保持IO完全結束後再建立與資料庫的讀寫連線或讀寫操作 管理好你的DBHelper
  2. DBHelper避免跨DB的操作,一個DB對應一個DBHelper例項

綜上多數情況都是程式碼上操作不當引發的問題,瞭解好原理才能對症下藥,避開危險區

補充: 減少多程式或多執行緒操作,儘可能單執行緒寫; 減少事務操作,減小事務複雜度。

相關文章