SQLite 是一款開源的 SQL 數據庫引擎,由於其自包含、無服務、零配置和友好的使用許可(完全免費)等特點,在桌面和移動平台被廣泛使用。
在應用開發過程中,如果想保存點數據,自然而然地就會想到 SQLite,畢竟它擁有非常多的實踐者。這里分享一個在項目開發過程中遇到的 SQLite 讀寫問題——在開發一個小型桌面應用系統時,需求是跟蹤文件系統中的變更,同時對變更文件進行相關操作,我們毫不猶豫地采用了 SQLite 來存儲文件變更信息。
在開發過程中,SQLite 的數據讀寫都非常順利,沒有什么障礙。然而,當業務邏輯一切就緒開始跑業務時,我們發現軟件處理業務的性能很差,每秒鍾只能處理 10 個左右的業務量,比數據放在內存的老系統還慢得多。老系統也還可以達到每秒三十幾個業務,而現在只有三分之一的水平。在有幾千幾萬個文件變更事件同時涌入的情況下,系統幾近停滯,會出現幾秒鍾一個業務的荒涼場景。這是不能容忍的事情。
經過技術排查,我們發現對 SQLite 的讀和寫都非常慢,最差的情況是從數據庫中獲取一條記錄要花掉 7 秒鍾,十分離譜。於是我們收羅學習了各種 SQLite 的優化技術並應用到了系統之中:
- SQL 操作時采用事務機制
sqlite3_exec(db,"BEGIN TRANSACTION;",0,0,0);
...
sqlite3_exec(db,"END TRANSACTION;",0,0,0);
- 批量操作時,使用sqlite3_prepare而不是sqlite3_exec
sqlite3_prepare_v2(db, zSQL, -1,&stmt, &pzTail);
sqlite3_step(stmt);
...
- 關閉數據庫的磁盤同步寫,降低數據安全性
sqlite3_exec(db,"PRAGMA synchronous = OFF; ",0,0,0);
常見的優化技術都已使用,效果有但不太理想,還是沒有達到老系統的性能,更不要說超過了。
這里需要回顧一下我們的應用模型。業務有並發處理的要求,系統中使用了多線程機制,這就出現了對 SQLite 並發多讀多寫的情況。我們查閱 SQLite 的官方文檔,多寫者的情況是不適用的。
至此,是不是說解決的出路就只有使用 client/server 這樣的數據庫了?小應用拖一個巨無霸數據庫,有種頭重腳輕的感覺。
記得數據庫課程的學習中,有提到大型數據庫訪問的 多層模型(N-tier),目的就是更高效地處理數據。那我們的文件型數據庫有沒有可能擁有 N-tier 的思想?盡管與大型數據庫的方法不一樣,但目的是一致的。我們分析一下現有應用對 SQLite 的讀寫情況,先看圖:
-
操作1
收到文件系統中的變更信息,並寫入到數據庫。由於文件變更信息是逐條發生的,無法預估事件的開始和結束,來一條寫一條的方式,導致開啟SQLite的事務模式也沒有啥效果。 -
操作2
讀取一條記錄並進行業務操作,這里的讀取並非只讀,需要將該條記錄標記為已選取,防止被其他業務處理線程讀取而引發重復處理。因此,這一步也存在寫操作。這里是讀一條處理一條。 -
操作3
業務處理完畢后,從數據庫中刪除。這里也是逐條刪除。
回顧應用的業務操作方式后發現,這些操作都是寫操作,而且還是逐條進行的。問題擺在這里,技術問題還是需要通過技術來解決。在優化的過程中,我們是分步驟進行的——
優化操作1
采用延遲寫的機制,收到文件變更信息后,不立即寫入數據庫,先放入緩存隊列,等到達一定時間后再進行批量寫入,這樣在大量事件涌入時效果明顯,大大減少了數據庫的寫操作次數。優化操作2
使用緩存;好不容易准備好數據庫查詢語句,只檢索了一條,太浪費時機,將符合檢索要求的記錄緩存起來。同時將記錄被選取的標記放在內存中而不寫數據庫,這樣對數據庫來說僅是讀操作。優化操作3
同樣采用延遲寫,將收到的刪除信息緩存起來,當累積到一定量或者時間后,再進行批量操作。這樣就可以充分利用 SQLite 的事務功能,大大提升寫操作的效率。
增加了這些數據庫訪問層后,數據庫的讀寫性能提升明顯,業務處理能力也達到了預期,超過了舊系統,主要的優化工作差不多就到此結束了。這里引入了延遲寫和緩存機制,增加了程序的復雜度,帶來的新挑戰是如何保持緩存記錄同數據庫記錄的一致性。為解決這個問題,使用了SQLite的自定義函數:
sqlite3_create_function(...);
通過創建自定義函數,來同步緩存記錄和數據庫記錄。比如:在從數據庫讀取業務記錄時,需要排除已經被標為"刪除"的記錄。
經歷這個項目,我們讓 SQLite 多讀多寫的並發訪問也成為了可能,算是一個收獲。(徐品華 | 天存信息)