文件数据库sqlite3 C++ 线程安全和并发

转载:http://www.javashuo.com/article/p-fecetapm-hy.html(线程安全和并发)html

转载:https://juejin.im/post/5b7d8522e51d4538e5679f5e(WAL模式介绍)sql

转载:https://blog.csdn.net/vannachen/article/details/8277344(多线程/WAL/锁)数据库

转载:https://blog.csdn.net/wql2rainbow/article/details/73650056(怎么开启wal机制)缓存

转载:https://www.cnblogs.com/cchust/category/802864.html(sqlite 详细教程)安全

转载:http://www.cnblogs.com/stephen-liu74/archive/2012/01/19/2326309.html(sqlite备份机制)多线程

转载:https://blog.csdn.net/northcan/article/details/7231115(打开数据库例子)并发

转载:https://blog.51cto.com/linzimo777/1544202(相同的项目场景,解决方法)app

转载:https://blog.csdn.net/wsmrcool/article/details/8287904(多线程多个数据库链接)函数

转载:https://www.jianshu.com/p/6a1ebd08f003(分库)post

转载:https://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=2649286361&idx=1&sn=78bbcda7f41a14291ad71289e4821f71&scene=21(多线程并发优化)

1、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 作显示事务,减小屡次的自动事务消耗

2、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()函数。

3、开启WAL机制

 int DataSource::InitDataBaseToWal(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_close(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_close(db);
 
                return -1;
            }
 
            rc = sqlite3_exec(db, "PRAGMA wal_autocheckpoint=100;", NULL, 0, &zErrMsg);
 
            if (rc != SQLITE_OK)
            {
                sqlite3_free(zErrMsg);
 
                sqlite3_close(db);
 
                return -1;
            }
        }
        else
        {
            rc = sqlite3_exec(db, "PRAGMA journal_mode=DELETE;", NULL, 0, &zErrMsg);
 
                   if (rc != SQLITE_OK)
               {
                sqlite3_free(zErrMsg);
 
                sqlite3_close(db);
 
                 return -1;
              }
            }
 
        return true;
      }
   

 4、多线程并发写操做的安全性

    sqlite实际支持的是多线程同时读但只支持同一时刻一个线程写,即所谓的多读单写,sqlite 支持 single-thread/multi-thread/serialized 三种不一样的线程安全模式。能够在编译sqlite组件时进行配置,或者能够经过 sqlite3_threadsafe()/sqlite3_config() 在程序运行时进行查看并配置线程安全模式。通过实际写 demo 测试,进行 multi-thread 或 serialized 配置之后,多线程并发读的场景下,没有问题。可是多线程并发写时依旧会抛错 database 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建立它的那个进程中使用。你不能在一个线程中打开一个数据库而后把指针传递给另外一个线程使用。这是由于大多数多线程系统的限制
相关文章
相关标签/搜索