原文转载于:https://www.cnblogs.com/5211314jackrose/p/5816066.html
1、异步I/O模式
通常,当sqlite写一个数据库文件时,会等待,直到写操作完成,然后控制返回到调用程序。相比于cpu操作,写文件系统是非常耗时的,这是一个性能瓶颈。异步I/O后端是sqlite的一个扩展模块,允许sqlite使用一个独立的后台线程来执行所有的写请求。虽然这并不会减少整个系统的资源消耗(cpu、磁盘带宽等),但它允许sqlite在正在写数据库时立刻返回到调用者,从用户角度看,无疑提高了前端的响应速度。对异步I/O,写请求在一个独立的后台线程中被处理,这意味着启动数据库写操作的线程不必等待磁盘I/O的发生。写操作看起来似乎很快就发生了,但实际上速度跟通常是一样的,只不过在后台进行。
异步I/O似乎提供了更好的响应能力,但这是有代价的。你会失去ACID中的持久性(Durable)属性。在sqlite的缺省I/O后端中,一旦写操作完成,你知道更改的数据已经安全地在磁盘上了。而异步I/O却不是这样的情况。如果应用程序在数据写操作之后,异步写线程完成之前发生崩溃或掉电,则数据库更改可能根本没有被写到磁盘,下一次使用数据库时就看不到更改。
异步I/O失去了持久性,但仍然保持ACID的其他三个属性:原子性(Atomic)、一致性(Consistent)和隔离性(Isolated)。很多应用程序没有持久性也能很好地工作。
我们通过创建一个sqlite VFS对象并且用sqlite3_vfs_register()注册它来使用异步I/O模式。当用这个VFS打开数据库文件并进行写操作时(使用vfs的xWrite()方法),数据不会立刻写到磁盘,而是放在由后台线程维护的写队列中。当用异步VFS打开数据库文件并进行读操作时(使用vfs的xRead()方法),数据从磁盘读出,而写队列从vfs读进程的角度看,其xWrite()已经完成了。异步I/O的虚拟文件系统(VFS)通过sqlite3async_initialize()来注册,通过sqlite3async_shutdown()来关闭。
为了积累经验,异步I/O的实现有意保持简单。更多的功能会在将来的版本中添加。例如,在当前的实现中,如果写操作正在一个稳定的流上发生,而这个流超过了后台写线程的I/O能力,则挂起的写操作队列将会无限地增长,可能会耗尽主机系统的内存。复杂一点的模块则可以跟踪挂起的写操作数量,在超过一定数目后停止接收新的写请求。
在单个进程中、使用异步IO的多个连接可以并发地访问单个数据库。从用户的角度看,如果所有连接都位于单个进程中,则正常sqlite和使用异步IO的sqlite,其并发性并没有什么不同。如果文件锁是激活的(缺省是激活的),来自多个进程的连接都要读和写数据库文件,则并发性在下面的情况下会减弱:
(1)当使用异步IO的连接启动一个数据库事务时,数据库会立刻被锁住。然而锁只有在写队列中的所有操作已经刷新到磁盘后才能释放。这意味着有时即使在一个"COMMIT"或"ROLLBACK"执行完后,数据库可能仍然处于锁住状态。
(2)如果应用程序使用异步IO连续地执行多个事务,其他数据库用户可能会因为数据库一直被锁住而不能使用数据库。这是因为当一个BEGIN执行后,数据库锁会立刻建立起来。但当对应的COMMIT或ROLLBACK发生时,锁不一定释放了,要到后台写队列全部刷新到磁盘后才能释放。如果后台写队列还没刷新完,数据库就一直处于锁住状态,其他进程不能访问数据库。
文件锁可以在运行时通过sqlite3async_control()函数禁用。对NFS这可以提高性能,因为可以避免对服务器的来回异步操作建立文件锁。但是如果多个连接尝试访问同一个数据库,而文件锁被禁用了,则应用程序崩溃和数据库损坏就可能发生。
异步IO扩展模块由单个源文件sqlite3async.c,和一个头文件sqlite3async.h组成,位于源码树的ext/async/子目录下。应用程序可以用其中定义的C API来激活和控制这个模块的功能。为了使用异步IO扩展,把sqlite3async.c编译成使用sqlite的应用程序的一部分,然后使用sqlite3async.h中定义的API来初始化和配置这个模块。这些API在sqlite3async.h的注释中有详细说明,使用这些API通常有以下步骤:
(1)调用sqlite3async_initialize()来给sqlite注册异步IO VFS(虚拟文件系统)。
(2)创建一个后台线程来执行写操作,并调用sqlite3async_run()。
(3)通过异步IO VFS,使用正常的sqlite API来读写数据库。
当前的异步IO扩展兼容win32系统和支持pthread接口的系统,包括Mac OS X,Linux和其他Unix变体。为了移植异步IO扩展到其他的平台,用户必须在新平台上实现互斥锁和条件变量原语。当前并没有外部可用接口来允许做这样的控制,但是修改sqlite3async.c中的代码以包含新平台的并发控制原语是相当容易的,更多细节可搜索sqlite3async.c中的注释串"PORTING FUNCTIONS"。然后实现下面这些函数的新版本:
static void async_mutex_enter(int eMutex); void async_mutex_leave(void async_cond_wait(int eCond,void async_cond_signal(int eCond); void async_sched_yield(void);
上面这些函数的功能在sqlite3async.c的注释中有详细描述。
2、共享缓存模式
从3.3.0版开始,sqlite包含一个特别的“共享缓存”模式(缺省情况下禁用),主要用在嵌入式服务器中。如果共享缓存模式激活,并且一个线程在同一个数据库上建立多个连接,则这些连接共享一个数据和模式缓存。这能够显著减少系统的内存和IO消耗。在3.5.0版中,共享缓存模式被修改以便同一缓存的共享可以跨越整个进程而不只是单个线程。在这个修改之前,在线程间传递数据连接是受限制的。从3.5.0版开始这个限制就消除了。
从另一个进程或线程的角度看,使用共享缓存的两个或多个数据库连接看起来就像是一个连接。锁协议用来在多个共享缓存或数据库用户之间进行仲裁。
图1 共享缓存模式
图1描述一个运行时配置的例子,有三个数据库连接。连接1是一个正常的sqlite数据库连接,连接2和3共享一个缓存。正常的锁协议用来在连接1和共享缓存之间串行化数据库访问。而连接2和连接3对共享缓存访问的串行化则有专门的内部协议。见下面的描述。
有三个级别的共享缓存加锁模型,事务级别的加锁,表级别的加锁和模式级别的加锁。
(1)事务级别的加锁
sqlite连接可能打开两种类型的事务,读事务和写事务。这不是显式完成的,一个事务隐式地含有一个读事务,直到它首次写一个数据库文件,这时成为一个写事务。在任何时候共享缓存上最多只能有一个连接打开一个写事务,这个写事务可以和任何数量的读事务共存。这与非共享缓存模式不同,非共享缓存模式下有读操作时不允许有写操作。
(2)表级别的加锁
当两个或更多的连接使用一个共享缓存,用锁来串行化每个表格的并发访问。表支持两种类型的锁,读锁和写锁。锁被授予连接,任何时候每个数据库连接上的每个表格可以有读锁、写锁或没有锁。一个表格上可以任何数量的读锁,但只能有一个写锁。读数据库表格时必须首先获得一个读锁。写表格时必须获得一个写锁。如果不能获取需要的锁,查询失败并返回sqlITE_LOCKED给调用者。表级别的锁在获取之后,要到当前事务(读或写)结束时才释放。
如果使用read_uncommitted pragma指令把事务隔离模式从串行(serialized,缺省模式,即查询数据时会加上共享琐,阻塞其他事务修改真实数据)改成允许脏读(read-uncommitted,即SELECT会读取其他事务修改而还没有提交的数据),则上面描述的行为会有稍许的变化。事务隔离模式还有另外两种,无法重复读read-comitted是同一个事务中两次执行同样的查询语句,若在第一次与第二次查询之间时间段,其他事务又刚好修改了其查询的数据且提交了,则两次读到的数据不一致。可以重复读read-repeatable是指同一个事务中两次执行同样的查询语句,得到的数据始终都是一致的。
/* Set the value of the read-uncommitted flag: ** ** True -> Set the connection to read-uncommitted mode. ** False -> Set the connection to serialized (the default) mode. */ PRAGMA read_uncommitted = <boolean>; Retrieve the current value of the read-uncommitted flag */ PRAGMA read_uncommitted;
允许脏读模式的数据库连接在读数据库表时不会获取读锁,如果这时另外一个数据库连接修改了正在被读的表数据,则可能导致查询结果不一致,因为允许脏读模式的读事务不会被打断。允许脏读模式不会影响写事务,它必须获取写锁,因此数据库写操作可以被阻塞。允许脏读模式也不会影响sqlite_master级别的锁。
(3)模式(sqlite_master)级别的加锁
sqlite_master表支持与其他数据库表相同的共享缓存读锁和写锁。还会使用下面的特殊规则:
* 在访问任何数据库表格或者获取任何其他的读锁和写锁之前,连接必须先获取一个sqlite_master表上的读锁。
* 在执行修改数据库模式的语句(例如CREATE TABLE或DROP TABLE)之前,连接必须先获取一个sqlite_master表上的写锁。
* 如果任何其他的连接持有关联数据库(包括缺省的主数据库)的sqlite_master表上的写锁,则连接不可以编译一个sql语句。
在sqlite 3.3.0到3.4.2之间,数据库连接只能被调用sqlite3_open()创建它的线程使用,一个连接只能与同一线程中的其他连接共享缓存。从sqlite 3.5.0开始,这个限制消除了。在老版本的sqlite上,共享缓存模式不能使用在虚拟表上,从sqlite 3.6.17开始,这个限制消除了。
共享缓存模式在每个进程级别上激活。C接口int sqlite3_enable_shared_cache(int)用来全局地激活或禁用共享缓存模式。每次调用sqlite3_enable_shared_cache()影响后续的使用sqlite3_open(),sqlite3_open16()或sqlite3_open_v2()创建的数据库连接,已经存在的数据库连接则不受影响。每次sqlite3_enable_shared_cache()的调用覆盖进程上的前面各次调用。
使用sqlite3_open_v2()创建的单个数据库连接,通过在第三个参数上使用sqlITE_OPEN_SHAREDCACHE或sqlITE_OPEN_PRIVATECACHE标志,可能选择参与或不参与共享缓存模式。在该数据库连接上这些标志会覆盖全局的sqlite3_enable_shared_cache()设置。如果同时使用这两个标志,则行为是未定义的。
当使用URI文件名时,"cache"查询参数可以用来指定连接是否使用共享缓存模式。"cache=shared"激活共享缓存,"cache=private"禁用共享缓存。例如:
ATTACH 'file:aux.db?cache=shared' AS aux;
从sqlite 3.7.13开始,倘若数据库使用URI文件名创建,共享缓存模式可以在内存数据库上使用。为了向后兼容,使用未修饰的":memory:"名称打开内存数据库时缺省是禁用共享缓存的。而在sqlite 3.7.13之前,无论使用的内存数据库名、当前系统的共享缓存设置、以及查询参数或标志是什么,内存数据库上共享缓存总是被禁用的。
在内存数据库上激活共享缓存,会允许同一进程上的两个或更多数据库连接访问同一段内存。当最后一个连接关闭时,内存数据库会自动删除,这段内存也会被重置。
3、解锁通知
当多个连接在共享缓存模式下访问同一个数据库时,单个表上的读锁和写锁(即共享锁和排他锁)用来确保并发执行的事务是隔离的。如果连接不能获取到需要的锁,sqlite3_step()调用返回sqlITE_LOCKED。如果不能获取到每个关联数据库的sqlite_master表上的读锁(虽然这种情况并不常见),sqlite3_prepare()或sqlite3_prepare_v2()调用也会返回sqlITE_LOCKED。
通过使用sqlite的sqlite3_unlock_notify()接口,我们可以让sqlite3_step()或sqlite3_prepare_v2()调用阻塞直到获得需要的锁,而不是立刻返回sqlITE_LOCKED。下面的例子展示解锁通知的使用。