SQLite 併發的四種處理方式

business-dog-paws-on-keyboard_925x.jpg

SQLite 是一款輕型的嵌入式數據庫它佔用資源很是的低,處理速度快,高效並且可靠。在嵌入式設備中,可能只須要幾百 K 的內存就夠了。所以在移動設備爆發時,它依然是最多見的數據持久化方案之一。不過即便 SQLite 已經很是成熟,可是咱們在編程中依然會遇到一些問題,其中最多見也最難搞的就是 —— 併發。html

就像其餘相似的問題同樣,SQLite 在移動端的併發處理也存在多種不一樣的設計。下面咱們經過 iOS 中四個經常使用類庫 (SQLite.swift, FMDB, GRDB, Core Data) 來看看這些設計。不過在此以前,咱們須要明確 SQLite 在併發編程環境下到底存在哪些問題:數據庫

  1. 併發寫操做:某一時刻可能存在對同一個數據庫的寫操做,而這是 SQLite 不容許的行爲。
  2. 操做隔離:連續的兩個數據庫查詢操做可能會出現結果差別,由於在併發環境下你沒法保證着兩個讀操做中間不會出現寫操做。
  3. 操做衝突:併發環境下數據庫的新增和修改操做執行的時序並不必定與調用時序是一致的。這就致使一個可能的情形就是:數據庫多個更新操做調用後可能存在一些意料以外的情形,並且你還難以追蹤排除。

明確這些問題後,接下來咱們就來看看這些類庫作出了何種應對。編程

SQLite.swift 方案

SQLite.swift 採用了最簡單粗暴的一種方案,使用者只會獲得一個數據庫鏈接,全部的操做都是在該鏈接上串下執行,類庫的做者並無提供數據庫鏈接池相似的特性。經過這種設計,任意時刻都只會存在一個線程對數據庫擁有訪問權限。也就是說上訴第一個併發問題被完美解決了。swift

然而改方案卻沒法應對第二個問題。例如,咱們須要爲數據庫中的某位用戶設置頭像,若是該用戶存在時則執行插入操做,對應代碼以下:安全

let userAvatars = avatars.filter(userId == 1)
let insert = avatars.insert(userId <- 1, url <- avatarURL)
if db.scalar(userAvatars.count) == 0 {
    try db.run(insert)
}
複製代碼

咋看之下代碼邏輯並無任何問題和缺陷,可是在併發環境下這裏存在一個隱藏的問題。你沒法保證在執行 * try db.run(insert)* 沒有任何地方執行相同的操做。雖然這種情形不多見並且數據庫在這種情形下也沒有 Crash 出現,可是可能在一開始數據庫在設定的時候就約定了每個用戶只能存在一條頭像信息,這就致使了業務邏輯錯誤或者衝突。bash

固然這個問題咱們能夠在數據庫定義時就能屏蔽掉,或者咱們顯式的經過事務對其進行處理:微信

try db.transaction {
    let userAvatars = avatars.filter(userId == 1)
    let insert = avatars.insert(userId <- 1, url <- avatarURL)
    if db.scalar(userAvatars.count) == 0 {
        try db.run(insert)
    }
}
複製代碼

可是有些時候,開發人員可能因工期等等問題而忽略上訴,最終埋下了隱患。對於第三個問題,類庫並無任何處理永遠都是 the last write always win多線程

FMDB 方案

FMDB 與 SQLite.swift 同樣都是採用串行設計,只不過 FMDB 在此基礎上作了些增強:FMDB 中使用者不會接觸到數據庫鏈接而是經過在 API 閉包中組織語句來實現數據庫訪問。閉包

dbQueue.inDatabase { db in
    if db.intForQuery("SELECT COUNT ...") == 0) {
        db.executeUpdate("INSERT INTO avatars ...")
    }
}
複製代碼

這種方式不只解決了同時寫的問題並且還很是平滑的解決了操做隔離問題,相比上一個方案明顯更爲友好。併發

GRDB 方案

此方案借鑑了 FMDB 中的 API 設計,使用者經過在閉包中組織語句來實現數據庫訪問。不過與前兩個相比,GRDB 最大的不一樣就是它再也不使用串行隊列設計。經過對 SQLite 自己 WAL 模式進行,GRDB 支持多線程同時進行讀寫操做。

注意:寫操做依然是串行進行,WAL 依然須要遵照 SQLite 單寫策略

try dbPool.write { db in
    if Int.fetchOne(db, "SELECT COUNT ...") == 0) {
        try db.execute("INSERT INTO avatars ...")
    }
}
複製代碼

該模式最大的特色在於,咱們在進行數據庫寫操做的同時,依然能並行的執行讀操做。這意味着,在特定線程運行費時的數據庫同步寫操做的時候用於更新 UI 的數據庫讀操做不會像前兩種方案同樣被阻塞住。也就是說,寫操做對於讀操做來講是透明的。

dbPool.read { db in
    // Those values are guaranteed to be equal:
    let count1 = User.fetchCount(db)
    let count2 = User.fetchCount(db)
}
複製代碼

而且 GRDB 經過 DatabaseSnapshot 對數據庫訪問進行了讀寫分離實現,進一步提升了多線程訪問的安全。

Core Data 方案

雖然 Apple 官方並無說 Core Data 是 SQLite 的一個封裝和實現,可是咱們都知道其實它底層仍是使用 SQLite 做爲存儲引擎。

爲了解決文章前面提到的 SQLite 併發情形下的典型問題,Core Data 本身實現並維護了一套上下文管理邏輯。 SQLite.swift 關注的上下文是其執行期間的單個SQL語句。 對於FMDB和GRDB 關注的上下文環境則是閉包中的 SQL 語句塊。 而 Core Data 託管上下文則是 NSManagedObjectContext 實例的整個生命週期,包含數據庫修改和內存修改。

這讓 Core Data 可以應對併發問題中的第三種情形,同一個對象若是在不一樣上下文中同時發生修改則會被檢測出來(文檔)。而前面三種方案只要 SQL 語句沒有違背表定義都能進行記錄更新並且最後一個永遠是贏家。

可是這種設計也存在缺點,首先擴大後的上下文管理是一件很是麻煩的事,另外全部的寫操做都會被嚴格束縛並且衝突處理依然很棘手,最後嚴格的上下文管理也讓 Core Data 中編寫正確的多線程代碼也變得很困難。

總結

每一類庫的做者都對 SQLite 併發處理有着本身的思考,因此沒有這裏並不存在一種標準處理方式。若是封裝過於簡單的話,那麼對使用者的要求就會比較高不然就會出現不少意想不到的錯誤或崩潰。封裝過於複雜的話則又有致使處理的靈活性變得不好。若是搞的大而全的話則有可能致使 SQLite 的執行效率變得不好。

整體而言,FMDB 和 GRDB 採用的方式從安全性和靈活性上會更好一點。順便提一下,根據微信團隊的文章他們採用的多是 GRDB 那種方式,由於在微信的應用場景下寫操做是瓶頸所在。

原文地址

相關文章
相關標籤/搜索