SQLite 中的隔离

数据库的“隔离”属性决定了一个操作对数据库所做的更改何时对其他并发操作可见。

数据库连接之间的隔离

如果使用两个不同的 数据库连接(分别调用sqlite3_open()返回两个不同的sqlite3对象)读取和写入同一个数据库,并且这两个数据库连接没有共享缓存,那么读者只能看到完整的提交来自作家的交易。作者未提交的部分更改对读者是不可见的。不管这两个数据库连接是在同一个线程中,还是在同一个进程的不同线程中,还是在不同的进程中,都是如此。这是 SQL 数据库系统的常见和预期行为。

只要 read_uncommitted pragma保持关闭,上一段在共享缓存模式下 也适用(单独的数据库连接彼此隔离) 。默认情况下,read_uncommitted pragma 是关闭的,因此如果应用程序不执行任何操作来打开它,它将保持关闭状态。因此,除非read_uncommitted pragma用于更改默认行为,否则一个数据库连接所做的更改对于共享同一缓存的不同数据库连接上的读取器是不可见的,直到写入器提交其事务。

如果两个数据库连接共享同一个缓存并且读取器启用了read_uncommitted pragma,那么读取器将能够在写入器事务提交之前看到写入器所做的更改。共享缓存模式read_uncommitted pragma的组合使用 是一个数据库连接可以看到不同数据库连接上未提交更改的唯一方法。在所有其他情况下,单独的数据库连接彼此完全隔离。

除了打开PRAGMA read_uncommitted的共享缓存数据库连接 的情况外,SQLite 中的所有事务都显示“可序列化”隔离。SQLite 通过实际序列化写入来实现可序列化事务。SQLite 数据库一次只能有一个写入器。可以同时打开多个数据库连接,所有这些数据库连接都可以写入数据库文件,但它们必须轮流进行。SQLite 使用锁自动序列化写入;这不是使用 SQLite 的应用程序需要担心的事情。

隔离与并发

SQLite 使用出现在与数据库文件相同的目录中的临时日志文件来实现隔离和并发控制(和原子性)。有两种主要的“期刊模式”。旧的“回滚模式”对应于对 journal_mode pragma使用“DELETE”、“PERSIST”或“TRUNCATE”选项。在回滚模式下,更改直接写入数据库文件,同时构建一个单独的回滚日志文件,如果事务回滚,能够将数据库恢复到原始状态。回滚模式(特别是 DELETE 模式,意味着回滚日志在每个事务结束时从磁盘中删除)是当前的默认行为。

3.7.0 版本(2010-07-21)开始,SQLite 也支持“ WAL 模式”。在 WAL 模式下,更改不会写入原始数据库文件。相反,更改进入单独的“预写日志”或“WAL”文件。稍后,在事务提交后,这些更改将在称为“检查点”的操作中从 WAL 文件移回原始数据库。WAL 模式通过运行“ PRAGMA journal_mode=WAL ”启用。

在回滚模式下,SQLite 通过锁定数据库文件并防止在每个写入事务进行时其他数据库连接进行任何读取来实现隔离。读取器可以在写入开始时处于活动状态,在任何内容被刷新到磁盘之前,同时所有更改仍保留在写入器的私有内存空间中。但是在对磁盘上的数据库文件进行任何更改之前,必须(暂时)驱逐所有读取器,以便为写入器提供对数据库文件的独占访问权限。因此,由于在事务写入磁盘时被锁定在数据库之外,读者无法看到不完整的事务。只有在事务完全写入并同步到磁盘并提交后,才允许读者返回数据库。

WAL 模式允许同时读取和写入。它可以这样做,因为更改不会覆盖原始数据库文件,而是进入单独的预写日志文件。这意味着在写入者追加到预写日志的同时,读取者可以继续从原始数据库文件中读取旧的、原始的、未更改的内容。WAL 模式下,SQLite 表现出“快照隔离”。当读取事务开始时,该读取器继续看到数据库文件的不变“快照”,因为它在读取事务开始时及时存在。在读取事务处于活动状态时提交的任何写入事务对于读取事务仍然是不可见的,因为读取器正在及时查看数据库文件的快照。

一个示例:假设有两个数据库连接 X 和 Y。X 使用BEGIN后跟一个或多个SELECT语句来启动读取事务。然后 Y 出现并运行UPDATE语句来修改数据库。X 随后可以对 Y 修改的记录执行SELECT,但 X 将看到较早的未修改条目,因为当 X 持有读取事务时,Y 的更改对 X 都是不可见的。如果 X 想要查看 Y 所做的更改,则 X 必须结束其读取事务并开始一个新事务(通过运行COMMIT后跟另一个BEGIN。)

另一个示例:X 使用BEGINSELECT 启动读取事务,然后 Y 使用UPDATE对数据库进行更改。然后 X 尝试使用UPDATE对数据库进行更改。X 尝试将其事务从读取事务升级为写入事务失败并出现 SQLITE_BUSY_SNAPSHOT错误,因为 X 正在查看的数据库快照不再是数据库的最新版本。如果允许 X 写入,它会分叉数据库文件的历史记录,这是 SQLite 不支持的。为了让 X 写入数据库,它必须首先释放它的快照(使用ROLLBACK例如)然后使用后续的BEGIN开始新事务。

如果 X 启动一个事务,该事务最初只会读取,但 X 知道它最终会想要写入,并且不想被可能出现的 SQLITE_BUSY_SNAPSHOT 错误所困扰,因为另一个连接在它前面跳转,那么 X 可以发出BEGIN IMMEDIATE来启动它的交易而不仅仅是一个普通的开始。BEGIN IMMEDIATE 命令继续并启动写事务,从而阻止所有其他写入器如果BEGIN IMMEDIATE操作成功,则该事务中的后续操作将永远不会因 SQLITE_BUSY错误而失败。

同一数据库连接上的操作之间没有隔离

SQLite 在单独的数据库连接中提供操作之间的隔离。但是,在同一数据库连接中发生的操作之间没有隔离。

换句话说,如果 X 使用BEGIN IMMEDIATE开始写入事务, 然后发出一个或多个UPDATEDELETE和/或INSERT 语句,那么这些更改对于在数据库连接 X 中评估的 后续SELECT语句是可见的。不同的SELECT语句在 X 事务提交之前,数据库连接 Y 不会显示任何更改。但是X 中的SELECT语句将显示提交之前的更改。

在单个数据库连接 X 中,SELECT 语句总是看到在 SELECT 语句开始之前完成的对数据库的所有更改,无论是已提交还是未提交。并且 SELECT 语句显然看不到 SELECT 语句完成后发生的任何变化。但是在 SELECT 语句运行时发生的变化呢?如果启动 SELECT 语句并且sqlite3_step() 接口逐步执行其输出的大约一半,然后应用程序运行一些更新 语句来修改 SELECT 语句正在读取的表,然后更多调用sqlite3_step()是为了完成 SELECT 语句?SELECT 语句的后续步骤是否会看到 UPDATE 所做的更改?答案是这种行为是未定义的。特别是,SELECT 语句是否看到并发更改取决于正在运行的 SQLite 版本、数据库文件的架构、是否已运行ANALYZE以及查询的详细信息。在某些情况下,它也可能取决于数据库文件的内容。没有好的方法可以知道 SELECT 语句是否会看到在 SELECT 语句启动后由同一数据库连接对数据库所做的更改。因此,开发人员应该努力避免编写对那种情况下会发生什么做出假设的应用程序。

如果应用程序在单个表上发出 SELECT 语句,如“ SELECT rowid, * FROM table WHERE ... ”,并开始使用sqlite3_step()单步执行该语句的输出并检查每一行,然后应用程序可以安全地使用“DELETE FROM table WHERE rowid=?”删除当前行或任何先前的行。应用程序删除预期会在查询中稍后出现但尚未出现的行也是安全的(从某种意义上说,它不会损害数据库)。但是,如果删除了未来的行,则该行可能会在后续的 sqlite3_step() 之后出现,即使它据称已被删除。或者它可能不会。该行为是未定义的。应用程序还可以在 SELECT 语句运行时将新行插入表中,但新行是否出现在查询的后续 sqlite3_step() 中是不确定的。并且应用程序可以更新当前行或任何先前的行,虽然这样做可能会导致该行重新出现在后续的 sqlite3_step() 中。只要应用程序准备好处理这些歧义,操作本身就是安全的,不会损害数据库文件。

出于前两段的目的,具有相同共享缓存并且启用 了PRAGMA read_uncommitted的两个数据库连接被认为是相同的数据库连接。

概括

  1. SQLite 中的事务是可序列化的。

  2. 在一个数据库连接中所做的更改在提交之前对所有其他数据库连接都是不可见的。

  3. 查询会看到在查询开始之前在同一数据库连接上完成的所有更改,无论这些更改是否已提交。

  4. 如果在查询开始运行之后但在查询完成之前同一数据库连接发生更改,则查询是否会看到这些更改是不确定的。

  5. 如果在查询开始运行之后但在查询完成之前同一数据库连接发生更改,则查询可能会多次返回更改的行,或者它可能会返回先前删除的行。

  6. 就前四项而言,使用相同共享缓存并启用PRAGMA read_uncommitted的两个数据库连接被认为是相同的数据库连接,而不是单独的数据库连接。