sqlite 锁机制_SQLite读写为什么冲突

2022-09-22 19:56:41 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。

sqlite读写锁

SQLite3总共有三种事务类型BEGIN [ DEFERRED /IMMEDIATE / EXCLUSIVE ] TRANSCATION,提供以下五种的文件锁状态,按锁的级别依次是:UNLOCKED / SHARED / RESERVERD / PENDING / EXCLUSIVE

1). UNLOCKED:无锁   文件没有持有任何锁,即当前数据库不存在任何读或写的操作。其它的进程可以在该数据库上执行任意的读写操作。此状态为缺省状态。 2). SHARED:共享锁   在此状态下,该数据库可以被读取但是不能被写入。在同一时刻可以有任意数量的进程在同一个数据库上持有共享锁,因此读操作是并发的。换句话说,只要有一个或多个共享锁处于活动状态,就不再允许有数据库文件写入的操作存在。 3). RESERVED:保留锁   假如某个进程在将来的某一时刻打算在当前的数据库中执行写操作,然而此时只是从数据库中读取数据,那么我们就可以简单的理解为数据库文件此时已经拥有了保留锁。当保留锁处于活动状态时,该数据库只能有一个或多个共享锁存在,即同一数据库的同一时刻只能存在一个保留锁和多个共享锁。   在Oracle中此类锁被称之为预写锁,不同的是Oracle中锁的粒度可以细化到表甚至到行,因此该种锁在Oracle中对并发的影响程序不像SQLite中这样大。 4). PENDING:等待锁(写等待)   PENDING锁的意思是说,某个进程正打算在该数据库上执行写操作,然而此时该数据库中却存在很多共享锁(读操作),那么该写操作就必须处于等待状态,即等待所有共享锁消失为止,与此同时,新的读操作将不再被允许,以防止写锁饥饿的现象发生。在此等待期间,该数据库文件的锁状态为PENDING,在等到所有共享锁消失以后,PENDING锁状态的数据库文件将在获取排他锁之后进入EXCLUSIVE状态。 5). EXCLUSIVE:排它锁(写)   在执行写操作之前,该进程必须先获取该数据库的排他锁。然而一旦拥有了排他锁,任何其它锁类型都不能与之共存。因此,为了最大化并发效率,SQLite将会最小化排他锁被持有的时间总量。

SQLite3 并发控制过程

SQLite的并发控制机制是采用加锁的方式,当执行select即读操作时,需要获取到SHARED锁(共享锁),当执行insert/update/delete操作(即内存写操作时),需要进一步获取到RESERVERD锁(保留锁),当进行commit操作(即磁盘写操作时),需要进一步获取到EXCLUSIVE锁(排它锁)。   对于RESERVERD锁,sqlite3保证同一时间只有一个连接可以获取到保留锁,也就是同一时间只有一个连接可以写数据库(内存),但是其它连接仍然可以获取SHARED锁,也就是其它连接仍然可以进行读操作(这里可以认为写操作只是对磁盘数据的一份内存拷贝进行修改,并不影响读操作)。   对于EXCLUSIVE锁,是比保留锁更为严格的一种锁,在需要把修改写入磁盘即commit时需要在保留锁/未决锁的基础上进一步获取到排他锁,顾名思义,排他锁排斥任何其它类型的锁,即使是SHARED锁也不行,所以,在一个连接进行commit时,其它连接是不能做任何操作的(包括读)。   PENDING锁(即未决锁),则是比较特殊的一种锁,它可以允许已获取到SHARED锁的事务继续进行,但不允许其它连接再获取SHARED锁,当已存在的SHARED锁都被释放后(事务执行完成),持有未决锁的事务就可以获得commit的机会了。sqlite3使用这种锁来防止writer starvation(写饿死)。

读操作

用共享锁(Shared lock),所以并发的多个读数据库。如果有一个读操作存在,那么都不会允许写。

写操作

1.它首先会申请一个预留锁(Reserved lock),在启用Reserved lock之后,已存在的读可以继续读,也可以有新的读请求。

2.然后,它会把需要更新的数据写到缓冲区中。

3.需要写到缓冲区的更新写完以后,就需要将更新刷到硬盘db了。这时,它会申请Pending lock,就不能再有新的Shared lock申请了,也就是阻止了新的读操作。但是已经存在的读操作还是可以继续读的。然后它就等待,直到没有读操作存在(即所有的读都已经结束)这个时候,它就会申请排他锁,此时不允许有其他锁的存在,然后进行commit,将缓冲区的数据写入db中。

举个例子:

B进行写操作,申请了预留锁;然后A进行读操作,申请了共享锁(有预留锁时,是允许读操作申请的);然后A又同时想进行写操作(未释放共享锁的情况),此时申请预留锁(因为已经有预留锁存在了)失败;B写完缓存,想commit时,申请了未决锁,但是无法从未决锁提升到排他锁(因为有共享锁存在)。此时,发生死锁,A和B都想等待对方释放锁。

死锁的情况

死锁的情况:当两个连接使用begin transaction开始事务时,第一个连接执行了一次select操作(已经获取到SHARED锁),第二个连接执行了一次insert操作(已经获取到了RESERVERD锁),此时第一个连接需要进行一次insert/update/delete(需要获取到RESERVERD锁),第二个连接则希望执行commit(需要获取到EXCLUSIVE锁),由于第二个连接已经获取到了RESERVERD锁,根据RESERVERD锁同一时间只有一个连接可以获取的特性,第一个连接获取RESERVERD锁的操作必定失败,而由于第一个连接已经获取到SHARED锁,第二个连接希望进一步获取到EXCLUSIVE锁的操作也必定失败。就导致了事务死锁。

sqlite线程模式

SQLite 与线程

SQLite 是线程安全的。

线程模型

SQLite 支持如下三种线程模型

  • 单线程模型 这种模型下,所有互斥锁都被禁用,同一时间只能由一个线程访问。
  • 多线程模型 这种模型下,一个连接在同一时间内只有一个线程使用就是安全的。
  • 串行模型 开启所有锁,可以随意访问。

单线程:这种模式下,没有进行互斥,多线程使用不安全。禁用所有的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。

  串行:sqlite是线程安全的。启用所有的锁,包括bCoreMutex和bFullMutex 。因为数据库连接和prepared statement都已加锁,所以多线程使用这些对象时没法并发,也就变成串行了。当SQLite编译时加了SQLITE_THREADSAFE =1参数时默认启用。若SQLITE_THREADSAFE不为0,可以在初始化SQLite前,调用sqlite3_config(SQLITE_CONFIG_SERIALIZED)启用;或者在创建数据库连接时,设置SQLITE_OPEN_FULLMUTEX flag 。

而这里所说的初始化是指调用sqlite3_initialize()函数,这个函数在调用sqlite3_open()时会自动调用,且只有第一次调用是有效的.

设置线程模型

SQLite 可以通过以下三种方式进行线程模型的设置,在实际应用中选择任一一项都可以。

  • 编译期设定 通过 SQLITE_THREADSAFE 这个参数进行编译器的设定来选择线程模型
  • 初始化设定 通过调用 sqlite3_config() 可以在 SQLite 初始化时进行设定
  • 运行时设定 通过调用 sqlite3_open_v2() 接口指定数据库连接的数据库模型

为了达到线程安全,SQLite在编译时必须将 SQLITE_THREADSAFE 预处理宏置为1。在Windows和Linux上, 已编译的好的二进制发行版中都是这样设置的。如果不确定你所使用的库是否是线程安全的,可以调用 sqlite3_threadsafe() 接口找出。调用sqlite3_threadsafe()可以获得编译期的SQLITE_THREADSAFE参数 。

也就是说线程模式可以在编译时(通过源码编译sqlite库时)、启动时(使用sqlite的应用程序初始化时)或者运行时(创建数据库连接时)来指定。一般而言,运行时指定的模式将覆盖启动时的指定模式,启动时指定的模式将覆盖编译时指定的模式。但是,单线程模式一旦被指定,将无法被覆盖。默认的线程模式是串行模式。

编译时选择线程模式 这几种模式可以通过参数SQLITE_THREADSAFE在编译阶段指定,可以取值0,1,2,默认是1。这三种取值的含义如下:

0:单线程模式,即内部不做mutex保护,多线程运行sqlite不安全。

1:多线程的串行模式,sqlite帮助多线程实现串行化。

2:多线程的并发模式,要求同一个时刻,同一个连接不被多个线程使用。

sqlite3_threadsafe()函数的返回值可以确定编译时指定的线程模式。如果指定了单线程模式,函数返回false。如果指定了串行或者多线程模式,函数返回true。由于sqlite3_threadsafe()函数要早于多线程模式以及启动时和运行时的模式选择,所以它既不能区别多线程模式和串行模式也不能区别启动时和运行时的模式。

最后一句可通过sqlite3_threadsafe函数的实现来理解

SQLITE_API int sqlite3_threadsafe(void){ return SQLITE_THREADSAFE; }

如果编译时指定了单线程模式,那么临界互斥逻辑在构造时就被省略,因此也就无法在启动时或运行时指定串行模式或多线程模式。

启动时选择线程模式 假如在编译时没有指定单线程模式,就可以在应用程序初始化时使用sqlite3_config()函数修改线程模式。参数SQLITE_CONFIG_SINGLETHREAD可指定为 单线程模式,SQLITE_CONFIG_MULTITHREAD指定为多线程模式,SQLITE_CONFIG_SERIALIZED指定为串行模式。

在打开数据库时(sqlite3_open_v2())通过参数指定,主要的几个参数以及含义如下:

SQLITE_OPEN_NOMUTEX: 设置数据库连接运行在多线程模式(没有指定单线程模式的情况下)

SQLITE_OPEN_FULLMUTEX:设置数据库连接运行在串行模式。

SQLITE_OPEN_SHAREDCACHE:设置运行在共享缓存模式。

SQLITE_OPEN_PRIVATECACHE:设置运行在非共享缓存模式。

SQLITE_OPEN_READWRITE:指定数据库连接可以读写。

SQLITE_OPEN_CREATE:如果数据库不存在,则创建。

运行时选择线程模式 如果没有在编译时和启动时指定为单线程模式,那么每个数据库连接在创建时可单独的被指定为多线程模式或者串行模式,但是不能指定为单线程模式。如果在编译时或启动时指定为单线程模式,就无法在创建连接时指定多线程或者串行模式。

创建连接时用sqlite3_open_v2()函数的第三个参数来指定线程模式。SQLITE_OPEN_NOMUTEX标识创建多线程模式的连接;SQLITE_OPEN_FULLMUTEX标识创建串行模式的连接。如果没有指定标识,或者使用sqlite3_open()或sqlite3_open16()函数来创建数据库连接,那么在编译时或启动时指定的线程模式将作为默认的线程模式使用。

通过调用sqlite3_config接口,也可以设置运行模式。

若编译参数SQLITE_THREADSAFE=1 or SQLITE_THREADSAFE=2,那么可以在运行时设置线程模式。

SQLITE_CONFIG_SINGLETHREAD:单线程模式

SQLITE_CONFIG_MULTITHREAD:多线程模式,应用层保证同一个时刻,同一个连接只有一个线程使用。

SQLITE_CONFIG_SERIALIZED:串行模式,sqlite帮助多线程实现串行化。

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 独占锁。表示它可以写入数据库了。进入这个状态后,其他任何线程都不能访问数据库文件。因此为了并发性,它的持有时间越短越好。

一个线程只有拥有低级别锁时才能够获得更高一级的锁

总结

综上所述,要保证数据库使用的安全,一般可以采用如下几种模式

  • SQLite 采用单线程模型,用专门的线程/队列(同时只能有一个任务执行访问) 进行访问
  • SQLite 采用多线程模型,每个线程都使用各自的数据库连接 (即 sqlite3 *)
  • SQLite 采用串行模型,所有线程都公用同一个数据库连接。

因为写操作的并发性并不好,当多线程进行访问时实际上仍旧需要互相等待,而读操作所需要的 SHARED 锁是可以共享的,所以为了保证最高的并发性,推荐

  • 使用多线程模式
  • 使用 WAL 模式
  • 单线程写,多线程读 (各线程都持有自己对应的数据库连接)
  • 避免长时间事务
  • 缓存 sqlite3_prepare 编译结果
  • 多语句通过 BEGIN 和 COMMIT 做显示事务,减少多次的自动事务消耗

注:SQLite处理并发读没有什么问题,但是如果你的应用需要并发写的话,那么SQLite就不适合你了。

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/169970.html原文链接:https://javaforall.cn

0 人点赞