SQLite 并发的四种处理方式

business-dog-paws-on-keyboard_925x.jpg

SQLite 是一款轻型的嵌入式数据库它占用资源非常的低,处理速度快,高效而且可靠。在嵌入式设备中,可能只需要几百 K 的内存就够了。因此在移动设备爆发时,它依然是最常见的数据持久化方案之一。不过即使 SQLite 已经非常成熟,但是我们在编程中依然会遇到一些问题,其中最常见也最难搞的就是 —— 并发。

就像其他类似的问题一样,SQLite 在移动端的并发处理也存在多种不同的设计。下面我们通过 iOS 中四个常用类库 (SQLite.swift, FMDB, GRDB, Core Data) 来看看这些设计。不过在此之前,我们需要明确 SQLite 在并发编程环境下到底存在哪些问题:

  1. 并发写操作:某一时刻可能存在对同一个数据库的写操作,而这是 SQLite 不允许的行为。
  2. 操作隔离:连续的两个数据库查询操作可能会出现结果差异,因为在并发环境下你无法保证着两个读操作中间不会出现写操作。
  3. 操作冲突:并发环境下数据库的新增和修改操作执行的时序并不一定与调用时序是一致的。这就导致一个可能的情形就是:数据库多个更新操作调用后可能存在一些意料之外的情形,而且你还难以追踪排除。

明确这些问题后,接下来我们就来看看这些类库做出了何种应对。

SQLite.swift 方案

SQLite.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 出现,但是可能在一开始数据库在设定的时候就约定了每一个用户只能存在一条头像信息,这就导致了业务逻辑错误或者冲突。

当然这个问题我们可以在数据库定义时就能屏蔽掉,或者我们显式的通过事务对其进行处理:

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

原文地址

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏文渊之博

探索SQL Server元数据(一)

  在数据库中,我们除了存储数据外,还存储了大量的元数据。它们主要的作用就是描述数据库怎么建立、配置、以及各种对象的属性等。本篇简单介绍如何使用和查询元数据,如...

1302
来自专栏Netkiller

数据库进程间通信解决方案之MQ

摘要 你是否想过当数据库中的数据发生变化的时候出发某种操作?但因数据无法与其他进程通信(传递信号)让你放弃,而改用每隔一段时间查询一次数据变化的方法?下面的插件...

3284
来自专栏MYSQL轻松学

Mysql5.5&Mysql5.6&Mysql5.7特性

Mysql5.5 特性,相对于Mysql5.1 性能提升 默认InnoDB plugin引擎。具有提交、回滚和crash恢复功能、ACID兼容。 行级锁(一致性...

4575
来自专栏Netkiller

数据库进程间通信解决方案之MQ

数据库进程间通信解决方案之MQ 摘要 你是否想过当数据库中的数据发生变化的时候出发某种操作?但因数据无法与其他进程通信(传递信号)让你放弃,而改用每隔一段时间查...

4184
来自专栏架构师之路

InnoDB并发如此高,原因竟然在这?

《InnoDB行锁,如何锁住一条不存在的记录?》埋了一个坑,没想到评论反响剧烈,大家都希望深挖下去。原计划写写InnoDB的锁结束这个case,既然呼声这么高,...

1503
来自专栏沈玉琛的专栏

当 MySQL 连接池遇上事务(一):神秘的幽灵锁

MySQL连接池是一个很好的设计,通过将大量短连接转化为少量的长连接,从而提高整个系统的吞吐率。一般各个团队都会对连接池进行封装,只提供简洁的接口供上层使用。但...

1.6K2
来自专栏c#开发者

Oracle 开放源代码项目

Oracle 开放源代码项目 这是无数个可扩展、使用以及构建于 Oracle 技术的开放源代码项目中的一个简短的示例。如果您有自己喜欢的开放源代码项目未在此处列...

8398
来自专栏Netkiller

数据库进程间通信解决方案之MQ

数据库进程间通信解决方案之MQ 摘要 你是否想过当数据库中的数据发生变化的时候出发某种操作?但因数据无法与其他进程通信(传递信号)让你放弃,而改用每隔一段时间查...

3557
来自专栏企鹅号快讯

一枚女程序员眼中的mysql,值得收藏

某群聊天内容 什么是数据库? ‍‍数据库(Database)是按照数据结构来组织、存储和管理数据的仓库, 每个数据库都有一个或多个不同的API用于创建,访问,管...

4158
来自专栏Laoqi's Linux运维专列

MySQL性能调优 – 你必须了解的15个重要变量

2.3K2

扫码关注云+社区

领取腾讯云代金券