多线程如何并发访问SQLite数据库
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程如何并发访问SQLite数据库相关的知识,希望对你有一定的参考价值。
SQLite作为一款小型的嵌入式数据库,本身没有提供复杂的锁定机制,无法内部管理多路并发下的数据操作同步问题,更谈不上优化,所以涉及到多路并发的情况,需要外部进行读写锁控制,否则SQLite会返回SQLITE_BUSY错误,以驳回相关请求。返回SQLITE_BUSY主要有以下几种情况:
1。当有写操作时,其他读操作会被驳回
2。当有写操作时,其他写操作会被驳回
3。当开启事务时,在提交事务之前,其他写操作会被驳回
4。当开启事务时,在提交事务之前,其他事务请求会被驳回
5。当有读操作时,其他写操作会被驳回
6。读操作之间能够并发执行
基于以上讨论,可以看出这是一个典型的读者写者问题,读操作要能够共享,写操作要互斥,读写之间也要互斥
可以设计如下的方案解决并发操作数据库被锁定的问题,同时保证读操作能够保持最大并发
1。采用互斥锁控制数据库写操作
2。只有拥有互斥锁的线程才能够操作数据库
3。写操作必须独立拥有互斥锁
4。读操作必须能够共享互斥锁,即在第一次读取的时候获取互斥锁,最后一次读取的时候释放互斥锁 参考技术A
单线程:禁用所有的mutex锁,并发使用时会出错。当SQLite编译时加了SQLITE_THREADSAFE=0参数,或者在初始化SQLite前调用sqlite3_config(SQLITE_CONFIG_SINGLETHREAD)时启用。
多线程:只要一个数据库连接不被多个线程同时使用就是安全的。源码中是启用bCoreMutex,禁用bFullMutex。实际上就是禁用数据库连接和prepared statement(准备好的语句)上的锁,因此不能在多个线程中并发使用同一个数据库连接或prepared statement。当SQLite编译时加了SQLITE_THREADSAFE=2参数时默认启用。若SQLITE_THREADSAFE不为0,可以在初始化SQLite前,调用sqlite3_config(SQLITE_CONFIG_MULTITHREAD)启用;或者在创建数据库连接时,设置SQLITE_OPEN_NOMUTEX flag。
串行:启用所有的锁,包括bCoreMutex和bFullMutex。因为数据库连接和prepared statement都已加锁,所以多线程使用这些对象时没法并发,也就变成串行了。当SQLite编译时加了SQLITE_THREADSAFE=1参数时默认启用。若SQLITE_THREADSAFE不为0,可以在初始化SQLite前,调用sqlite3_config(SQLITE_CONFIG_SERIALIZED)启用;或者在创建数据库连接时,设置SQLITE_OPEN_FULLMUTEX flag。
SQLite 线程安全和并发
SQLite 与线程
SQLite 是线程安全的。
线程模型
SQLite 支持如下三种线程模型
- 单线程模型 这种模型下,所有互斥锁都被禁用,同一时间只能由一个线程访问。
- 多线程模型 这种模型下,一个连接在同一时间内只有一个线程使用就是安全的。
- 串行模型 开启所有锁,可以随意访问。
设置线程模型
SQLite 可以通过以下三种方式进行线程模型的设置,在实际应用中选择任一一项都可以。
- 编译期设定 通过 SQLITE_THREADSAFE 这个参数进行编译器的设定来选择线程模型
- 初始化设定 通过调用 sqlite3_config() 可以在 SQLite 初始化时进行设定
- 运行时设定 通过调用 sqlite3_open_v2() 接口指定数据库连接的数据库模型
SQLite 并发和事务
事务
事务是 SQLite 的核心概念。对数据库的操作 (绝大部分) 会被打包成一个事务进行提交,需要注意的是,这里的打包成事务是自动开启的。举例而言,如果简单在一个 for 循环语句里向数据库中插入 10 条数据,意味着将自动生成 10 个事务。但需要注意的是事务是非常耗时的,一般而言, SQLite 每秒能够轻松支持 50000 条的数据插入,但是每秒仅能够支持几十个事务。一般而言,事务速度受限于磁盘速度。所以在批量插入时需要考虑禁用自动提交,将其用 BEGIN ... COMMIT 打包成一个事务。
回滚模式和 WAL
为了保证写入正确,SQLite 在使用事务进行数据库改写时将拷贝当前数据库文件的备份,即 rollback journal,当事务失败或者发生意外需要回滚时则将备份文件内容还原到数据库中,并同时删除该日志。这是默认的 DELETE 模式。
而后 SQLite 也引入了 WAL 模式,即 Write-Ahead Log。在这种模式下,所有的修改会写入一个单独的 WAL 文件内。这种模式下,写操作甚至可以不去操作数据库,这使得所有的读操作可以在 "写的同时" 直接对数据库文件进行操作,得到更好的并发性能。
锁和并发
SQLite 通过五种锁状态来完成事务。
- UNLOCKED ,无锁状态。数据库文件没有被加锁。
- SHARED 共享状态。数据库文件被加了共享锁。可以多线程执行读操作,但不能进行写操作。
- RESERVED 保留状态。数据库文件被加保留锁。表示数据库将要进行写操作。
- PENDING 未决状态。表示即将写入数据库,正在等待其他读线程释放 SHARED 锁。一旦某个线程持有 PENDING 锁,其他线程就不能获取 SHARED 锁。这样一来,只要等所有读线程完成,释放 SHARED 锁后,它就可以进入 EXCLUSIVE 状态了。
- EXCLUSIVE 独占锁。表示它可以写入数据库了。进入这个状态后,其他任何线程都不能访问数据库文件。因此为了并发性,它的持有时间越短越好。
一个线程只有拥有低级别锁时才能够获得更高一级的锁
/*
** Lock the file with the lock specified by parameter eFileLock - one
** of the following:
**
** (1) SHARED_LOCK
** (2) RESERVED_LOCK
** (3) PENDING_LOCK
** (4) EXCLUSIVE_LOCK
**
** Sometimes when requesting one lock state, additional lock states
** are inserted in between. The locking might fail on one of the later
** transitions leaving the lock state different from what it started but
** still short of its goal. The following chart shows the allowed
** transitions and the inserted intermediate states:
**
** UNLOCKED -> SHARED
** SHARED -> RESERVED
** SHARED -> (PENDING) -> EXCLUSIVE
** RESERVED -> (PENDING) -> EXCLUSIVE
** PENDING -> EXCLUSIVE
**
** This routine will only increase a lock. Use the sqlite3OsUnlock()
** routine to lower a locking level.
*/
总结
综上所述,要保证数据库使用的安全,一般可以采用如下几种模式
- SQLite 采用单线程模型,用专门的线程/队列(同时只能有一个任务执行访问) 进行访问
- SQLite 采用多线程模型,每个线程都使用各自的数据库连接 (即 sqlite3 *)
- SQLite 采用串行模型,所有线程都公用同一个数据库连接。
因为写操作的并发性并不好,当多线程进行访问时实际上仍旧需要互相等待,而读操作所需要的 SHARED 锁是可以共享的,所以为了保证最高的并发性,推荐
- 使用多线程模式
- 使用 WAL 模式
- 单线程写,多线程读 (各线程都持有自己对应的数据库连接)
- 避免长时间事务
- 缓存 sqlite3_prepare 编译结果
- 多语句通过 BEGIN 和 COMMIT 做显示事务,减少多次的自动事务消耗
参考
Using SQLite In Multi-Threaded Applications
https://blog.csdn.net/u011342466/article/details/79740086
以上是关于多线程如何并发访问SQLite数据库的主要内容,如果未能解决你的问题,请参考以下文章