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 多讀多寫的併發訪問也成為了可能,算是一個收穫。(徐品華 | 天存資訊)