⽂件数据库sqlite3C++线程安全和并发
⼀、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 inrted 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 inrted intermediate states:
**
** UNLOCKED -> SHARED
** SHARED -> RESERVED
** SHARED -> (PENDING) -> EXCLUSIVE
** RESERVED -> (PENDING) -> EXCLUSIVE
** PENDING -> EXCLUSIVE
**
** This routine will only increa a lock. U the sqlite3OsUnlock()
** routine to lower a locking level.
*/
总结
综上所述,要保证数据库使⽤的安全,⼀般可以采⽤如下⼏种模式
SQLite 采⽤单线程模型,⽤专门的线程/队列(同时只能有⼀个任务执⾏访问)进⾏访问
SQLite 采⽤多线程模型,每个线程都使⽤各⾃的数据库连接(即 sqlite3 *)
SQLite 采⽤串⾏模型,所有线程都公⽤同⼀个数据库连接。
因为写操作的并发性并不好,当多线程进⾏访问时实际上仍旧需要互相等待,⽽读操作所需要的 SHARED 锁是可以共享的,所以为了保证最⾼的并发性,推荐
使⽤多线程模式
使⽤ WAL 模式
单线程写,多线程读(各线程都持有⾃⼰对应的数据库连接)
避免长时间事务
缓存 sqlite3_prepare 编译结果
多语句通过 BEGIN 和 COMMIT 做显⽰事务,减少多次的⾃动事务消耗
⼆、WAL 机制的原理是:
修改并不直接写⼊到数据库⽂件中,⽽是写⼊到另外⼀个称为 WAL 的⽂件中;如果事务失败,WAL 中的记录会被忽略,撤销修改;如果事务成功,它将在随后的某个时间被写回到数据库⽂件中,提交修改。同步 WAL ⽂件和数据库⽂件的⾏为被称为 checkpoint(检查点),它由 SQLite ⾃动执⾏,默认是在 WAL ⽂件积累到 1000 页修改的时候;当然,在适当的时候,也可以⼿动执⾏ checkpoint,SQLite 提供了相关的接⼝。执⾏ checkpoint 之后,WAL ⽂件会被清空。在读的时候,SQLite 将在 WAL ⽂件中搜索,找到最后⼀个写⼊点,记住它,并忽略在此之后的写⼊点(这保证了读写和读读可以并⾏执⾏);随后,它确定所要读的数据所在页是否在 WAL ⽂件中,如果在,则读 WAL ⽂件中的数据,如果不在,则直接读数据库⽂件中的数据。在写的时候,SQLite 将之写⼊到 WAL ⽂件中即可,但是必须保证独占写⼊,因此写写之间不能并⾏执⾏。
2.1 wal⼯作原理
在引⼊WAL机制之前,SQLite使⽤rollbackjournal机制实现原⼦事务。
rollback journal机制的原理是:在修改数据库⽂件中的数据之前,先将修改所在分页中的数据备份在另外⼀个地⽅,然后才将修改写⼊到数据库⽂件中;如果事务失败,则将备份数据拷贝回来,撤销修改;如果事务成功,则删除备份数据,提交修改。
WAL机制的原理是:修改并不直接写⼊到数据库⽂件中,⽽是写⼊到另外⼀个称为WAL的⽂件中;如果事务失败,WAL中的记录会被忽略,撤销修改;如果事务成功,它将在随后的某个时间被写回到数据库⽂件中,提交修改。
2.2 wal优点:
1. 读和写可以完全地并发执⾏,不会互相阻塞(但是写之间仍然不能并发)。
2. WAL在⼤多数情况下,拥有更好的性能(因为⽆需每次写⼊时都要写两个⽂件)。
3. 磁盘I/O⾏为更容易被预测。
2.3 wal缺点:
儿童阅读
1. 访问数据库的所有程序必须在同⼀主机上,且⽀持共享内存技术。炒宽粉的做法
2. 每个数据库现在对应3个⽂件:<yourdb>.db,<yourdb>-wal,<yourdb>-shm。
3. 当写⼊数据达到GB级的时候,数据库性能将下降。
4. 3.7.0之前的SQLite⽆法识别启⽤了WAL机制的数据库⽂件。
寂寞难耐歌词2.4 wal如何记录数据--checkpoint
使⽤WAL模式时,改写操作是附加(append)到WAL⽂件,⽽不改动数据库⽂件,因此数据库⽂件可以被同时读取。当执⾏checkpoint操作时,WAL⽂件的内容会被写回数据库⽂件。当WAL⽂件达到SQLITE_DEFAULT_WAL_AUTOCHECKPOINT(默认值是1000)页(默认⼤⼩是1KB)时,会⾃动使⽤当前COMMIT的线程来执⾏checkpoint操作。也可以关闭⾃动checkpoint,改为⼿动定期checkpoint。
为了避免读取的数据不⼀致,查询时也需要读取WAL⽂件,并记录⼀个结尾标记(end mark)。这样的代价就是读取会变得稍慢,但是写⼊会变快很多。要提⾼查询性能的话,可以减⼩WAL⽂件的⼤⼩,但写⼊性能也会降低。需要注意的是,低版本的SQLite不能读取⾼版本的SQLite⽣成的WAL⽂件,但是数据库⽂件是通⽤的。这种情况在⽤户进⾏iOS降级时可能会出现,可以把模式改成delete,
再改回WAL来修复。
要对⼀个数据库连接启⽤WAL模式,需要执⾏“PRAGMA journal_mode=WAL;”这条命令,它的默认值是“journal_mode=DELETE”。执⾏后会返回新的journal_mode字符串值,即成功时为"wal",失败时为之前的模式(例如"delete")。⼀旦启⽤WAL模式后,数据库会保持这个模式,这样下次打开数据库时仍然是 WAL模式。要停⽌⾃动checkpoint,可以使⽤wal_autocheckpoint指令或sqlite3_wal_checkpoint()函数。⼿动执⾏ checkpoint可以使⽤wal_checkpoint指令或sqlite3_wal_checkpoint()函数。
三、开启WAL机制
int DataSource::InitDataBaToWal(std::string sPath, bool isWal)
{
char* zErrMsg;
sqlite3* db = NULL;
int rc = sqlite3_open_v2(sPath.c_str(), &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX, NULL);
if (rc != SQLITE_OK)
{
Logger::LogD("DataSource::sqlite [%s] or [%s] open failed", sPath.c_str(), sqlite3_errmsg(db));
Logger::LogO("DataSource::sqlite [%s] or [%s] open failed", sPath.c_str(), sqlite3_errmsg(db));
sqlite3_clo(db);
return -1;
}
if(isWal == true)
{
rc = sqlite3_exec(db, "PRAGMA journal_mode=WAL;", NULL, 0, &zErrMsg);
if (rc != SQLITE_OK)
{
sqlite3_free(zErrMsg);
sqlite3_clo(db);
return -1;
}
rc = sqlite3_exec(db, "PRAGMA wal_autocheckpoint=100;", NULL, 0, &zErrMsg);
if (rc != SQLITE_OK)
{
sqlite3_free(zErrMsg);
sqlite3_clo(db);
return -1;
}
}
el
{
rc = sqlite3_exec(db, "PRAGMA journal_mode=DELETE;", NULL, 0, &zErrMsg);
if (rc != SQLITE_OK)
{冰淇淋怎么写
sqlite3_free(zErrMsg);
谁家月下sqlite3_clo(db);
return -1;
}
}
return true;
沂岭杀四虎}
四、多线程并发写操作的安全性
sqlite实际⽀持的是多线程同时读但只⽀持同⼀时刻⼀个线程写,即所谓的多读单写,sqlite ⽀持 single-thread/multi-thread/rialized 三种不同的线程安全模式。可以在编译sqlite组件时进⾏配置,或者可以通过 sqlite3_threadsafe()/sqlite3_config() 在程序运⾏时进⾏查看并配置线程安全模式。经过实际写 demo 测试,进⾏ multi-thread 或 rialized 配置以后,多线程并发读的场景下,没有问题。但是多线程并发写时依旧会抛错 databa is locked。事实证明Sqlite不⽀持并发执⾏写⼊操作,即使是不同的表,只⽀持库级锁,⽽且这个Sqlite本⾝没有实现,必须⾃⼰实现这个库级锁,通过查阅官⽹资料,发现sqlite提供两个 busy handle 函数sqlite3_busy_timeout()/sqlite3_busy_handle()在并发访问失败时,会调⽤注册的 busy handle 函数,在注册的⾃定义的 busy handle 函数中可以进⾏处理(如重试n次等),这种处理⽅式必须建⽴在多线程多个数据库连接,多个数据库连接可以理解成,⽤sqlite3_open或者sqlite3_open_v2打开同⼀个数据库⽂件,每⼀个线程维护⼀个数据库连接对象,这样发⽣写竞争冲突的时候,可以通过回调函数重试,解决并发写。
线程安全:是指⼆个或三个线程可以同时调⽤独⽴的不同的sqlite3_open() 返回的"sqlite3"结构。⽽不是在多线程中同时使⽤同⼀个 sqlite3结构指针。
⼀个sqlite3结构只能在调⽤ sqlite3_open创建它的那个进程中使⽤。你不能在⼀个线程中打开⼀个数据库然后把指针传递给另⼀个线程使⽤。这是因为⼤多数多线程系统的限制