小巧、快速、可靠。
三选二。

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 文件移动回原始数据库,此操作称为“检查点”。通过运行“PRAGMA journal_mode=WAL”启用 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。 BEGIN IMMEDIATE命令会启动写入事务,从而阻止所有其他写入器。如果BEGIN IMMEDIATE操作成功,则该事务中的后续操作将永远不会因SQLITE_BUSY错误而失败。

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

SQLite 提供了不同数据库连接上的操作之间的隔离。但是,同一数据库连接内发生的操作之间没有隔离。

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

在单个数据库连接 X 内,SELECT 语句始终可以看到在 SELECT 语句开始之前完成的对数据库的所有更改,无论这些更改是否已提交。并且 SELECT 语句显然不会看到在 SELECT 语句完成之后发生的任何更改。但是,在 SELECT 语句运行期间发生的更改呢?如果 SELECT 语句已启动,并且sqlite3_step()接口遍历了其输出的大约一半,然后应用程序运行了一些UPDATE语句来修改 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() 之后出现,即使它据称已被删除。或者可能不会出现。该行为未定义。应用程序还可以 INSERT 新行到表中,而 SELECT 语句正在运行,但新行是否出现在查询的后续 sqlite3_step() 中是未定义的。并且应用程序可以 UPDATE 当前行或任何先前行,尽管这样做可能会导致该行在后续的 sqlite3_step() 中重新出现。只要应用程序准备处理这些歧义,这些操作本身就是安全的,并且不会损害数据库文件。

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

总结

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

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

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

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

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

  6. 出于前面四项目的考虑,两个使用相同共享缓存并启用PRAGMA read_uncommitted的数据库连接被视为同一个数据库连接,而不是独立的数据库连接。