SQLite开始并发

2020-10-19 22:56:14

通常,SQLite最多允许一个编写器并发进行。如果数据库处于";wal&34;或";wal2&34;模式,则BEGIN并发增强功能允许多个编写器同时处理写事务,尽管系统仍会序列化COMMIT命令。

当使用";BEGIN CONTRENT";打开写事务时,实际锁定数据库将推迟到执行COMMIT之后。这意味着使用BEGIN CURRENT启动的任意数量的事务可以并发进行。系统使用乐观页级锁定来防止冲突的并发事务被提交。

提交BEGIN CURRENT TRANSACTION时,系统会检查自打开BEGIN CURRENT以来,该事务已读取的任何数据库页是否已被修改。换句话说,它询问正在提交的事务是否操作与所有其他并发执行的事务不同的数据集。如果答案是";是,则该事务没有读取或修改任何并发事务";修改的任何数据,则事务照常提交。否则,如果事务确实发生冲突,则无法提交,并返回SQLITE_BUSY_SNAPSHOT错误。此时,客户端所能做的就是回滚事务。

如果返回SQLITE_BUSY_SNAPSHOT,则通过sqlite3_log机制输出消息,指示发生冲突的页和表或索引。这在优化并发性时很有用。

为了序列化提交处理,SQLite将数据库锁作为每个提交命令的一部分,并在返回之前将其释放。一次最多只能有一个写入者持有此锁。如果写入器无法获得锁,它会使用SQLite的忙处理程序暂停并重试一段时间:

如果存在严重的写入器锁争用,则此机制可能效率低下。在这种情况下,应用程序最好使用互斥或其他支持阻塞的机制,以确保一次最多有一个写入器尝试提交一个BEGIN CONTRENT TRANSACTION。如果所有编写器都是同一操作系统进程的一部分,这通常会更容易。

如果所有数据库客户端(读取器和写入器)都位于同一OS进程中,且该操作系统是Unix变体,则使用内置VFS";unix-excl&34;而不是默认的unix&34;会更高效。这是因为它使用了更高效的锁定原语。

使用BEGIN CURRENT最大化并发的关键是确保有大量无冲突的事务。在SQLite中,每个表和每个索引都存储为单独的b树,每个b树分布在一组离散的数据库页上。这意味着:

仅当键(主键或索引行)的值相当接近时,写入相同表或索引的两个事务才会冲突。例如,给定一个具有模式的大表:

为";a";写两行相邻的值可能会导致冲突(因为这两个键存储在同一页上),但是用非常不同的值为";a";写两行则不会(因为键很可能存储在不同的页上)。

请注意,在SQLite中,如果没有为INTEGERPRIMARY键显式提供值,例如:

然后自动分配单调递增的值。这不利于并发性,因为它几乎可以确保将所有新行添加到同一数据库页。在这种情况下,最好将随机值显式分配给整型主键字段。

对于没有显式整数主键列的非无ROWID表,也会出现此问题。在这些情况下,每个表都有一个隐式INTEGER主键列,该列被分配递增的值,导致与省略为显式整数主KEY列赋值相同的问题。

对于显式和隐式整数主键,都可以让SQLite随机赋值(而不是单调递增的值),方法是将rowid等于可能的最大签名64位整数的行写入表。例如:

某些类型的索引(例如,时间戳字段上的索引)的性质也可能导致问题(因为并发事务可能会将将存储在同一DB页上的相似标记分配给新记录)。在这些情况下,可能需要重新考虑数据库模式,以提高页级锁定提供的并发性。