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 那种方式,由于在微信的应用场景下写操做是瓶颈所在。

原文地址

相关文章
相关标签/搜索