1. SQLite 共享缓存模式
从版本 3.3.0 (2006-01-11) 开始,SQLite 包含一个特殊的“共享缓存”模式(默认禁用),用于嵌入式服务器。如果启用了共享缓存模式并且一个线程建立了到同一个数据库的多个连接,则这些连接共享一个数据和模式缓存。这可以显着减少系统所需的内存和 IO 数量。
在版本 3.5.0 (2007-09-04) 中,修改了共享缓存模式,以便可以在整个进程中共享相同的缓存,而不仅仅是在单个线程中。在此更改之前,在线程之间传递数据库连接存在限制。这些限制在 3.5.0 更新中被删除。本文档描述了 3.5.0 版的共享缓存模式。
在某些情况下,共享缓存模式会更改锁定模型的语义。详细信息在本文档中进行了描述。 假定对正常的 SQLite 锁定模型有基本的了解(有关详细信息,请参阅 SQLite 版本 3 中的文件锁定和并发)。
2.共享缓存锁定模型
在外部,从另一个进程或线程的角度来看,使用共享缓存的两个或多个数据库连接显示为单个连接。用于在多个共享缓存或常规数据库用户之间进行仲裁的锁定协议在其他地方有描述。
图1
图 1 描述了一个示例运行时配置,其中已建立三个数据库连接。连接 1 是一个普通的 SQLite 数据库连接。连接 2 和 3 共享缓存 普通锁定协议用于序列化连接 1 和共享缓存之间的数据库访问。用于序列化(或不序列化,请参阅下面的“读取未提交隔离模式”)连接 2 和 3 对共享缓存的访问的内部协议将在本节的其余部分描述。
共享缓存锁定模型分为三个级别,事务级锁定、表级锁定和模式级锁定。它们在以下三个小节中进行了描述。
2.1. 事务级锁定
SQLite连接可以开启两种事务,读事务和写事务。这不是显式完成的,一个事务隐含地是一个读事务,直到它第一次写入数据库表,此时它变成一个写事务。
最多一个到单个共享缓存的连接可以在任何时候打开一个写事务。这可能与任意数量的读取事务共存。
2.2. 表级锁定
当两个或多个连接使用共享缓存时,锁用于在每个表的基础上序列化并发访问尝试。表支持两种类型的锁,“读锁”和“写锁”。锁被授予连接——在任何时候,每个数据库连接在每个数据库表上要么有读锁,要么有写锁,要么没有锁。
在任何时候,单个表可能有任意数量的活动读锁或单个活动写锁。要从表中读取数据,连接必须首先获得读锁。要写入表,连接必须获得该表的写锁。如果无法获得所需的表锁,则查询失败并将 SQLITE_LOCKED 返回给调用者。
连接一旦获得表锁,直到当前事务(读或写)结束后才会释放。
2.2.1. 读未提交隔离模式
通过使用read_uncommitted pragma 将隔离级别从序列化(默认)更改为读取未提交,可以稍微修改上述行为 。
如上所述,未提交读模式下的数据库连接在读取数据库表之前不会尝试获取读锁。如果另一个数据库连接在读取表时修改了表,这可能会导致查询结果不一致,但这也意味着由处于未提交读模式的连接打开的读取事务既不能阻塞,也不能被任何其他连接阻塞。
未提交读模式对写入数据库表所需的锁没有影响(即未提交读连接仍必须获得写锁,因此数据库写入可能仍会阻塞或被阻塞)。此外,未提交读模式对下面列举的规则所需的sqlite_schema锁没有影响 (请参阅“Schema (sqlite_schema) Level Locking”部分)。
/* 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;
2.3. 架构 (sqlite_schema) 级别锁定
sqlite_schema表以与所有其他数据库表相同的方式支持共享缓存读写锁(参见上面的描述)。以下特殊规则也适用:
- 在访问任何数据库表或获得任何其他读或写锁之前,连接必须在sqlite_schema上获得读锁。
- 在执行修改数据库模式的语句(即 CREATE 或 DROP TABLE 语句)之前,连接必须获得 sqlite_schema上的写锁。
- 如果任何其他连接在任何附加数据库(包括默认数据库“main”) 的sqlite_schema表上持有写锁,则该连接可能无法编译 SQL 语句。
3.线程相关问题
在 SQLite 版本 3.3.0 到 3.4.2 中,当启用共享缓存模式时,数据库连接只能由调用sqlite3_open()创建它的线程使用。并且一个连接只能与同一线程中的另一个连接共享缓存。这些限制从 SQLite 版本 3.5.0 (2007-09-04) 开始被取消。
4.共享缓存和虚拟表
在旧版本的 SQLite 中,共享缓存模式不能与虚拟表一起使用。SQLite版本 3.6.17 (2009-08-10) 中删除了此限制。
5.启用共享缓存模式
共享缓存模式在每个进程的基础上启用。使用 C 接口,以下 API 可用于全局启用或禁用共享缓存模式:
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()建立的全局共享缓存模式设置。不应使用超过一个标志;如果在sqlite3_open_v2()的第三个参数中使用了 SQLITE_OPEN_SHAREDCACHE 和 SQLITE_OPEN_PRIVATECACHE 标志, 则行为未定义。
当使用URI 文件名时,“缓存”查询参数可用于指定数据库是否使用共享缓存。使用“cache=shared”启用共享缓存,使用“cache=private”禁用共享缓存。使用 URI 查询参数来指定数据库连接的缓存共享行为的能力允许在ATTACH语句中控制缓存共享。例如:
ATTACH 'file:aux.db?cache=shared' AS aux;
6.共享缓存和内存数据库
从 SQLite版本 3.7.13 (2012-06-11) 开始,共享缓存可以用于 内存数据库,前提是数据库是使用URI 文件名创建的。为了向后兼容,如果使用朴素的名称“:memory:”打开数据库,则内存数据库始终禁用共享缓存。在版本 3.7.13 之前,内存数据库始终禁用共享缓存,无论使用的数据库名称、当前系统共享缓存设置或查询参数或标志如何。
为内存数据库启用共享缓存允许同一进程中的两个或多个数据库连接访问同一个内存数据库。当与该数据库的最后一个连接关闭时,共享缓存中的内存数据库将自动删除并回收内存。