【SQLite 内核】事务与隔离:DEFERRED/IMMEDIATE/EXCLUSIVE 与单写者快照

💡 原文中文,约11200字,阅读约需27分钟。
📝

内容提要

本文介绍SQLite事务的三种BEGIN模式:DEFERRED推迟取锁,IMMEDIATE立即获取RESERVED锁,EXCLUSIVE在WAL模式下与IMMEDIATE等价。SQLite通过写串行化实现SERIALIZABLE隔离,单写者模型天然避免write skew异象。WAL模式下快照过期会返回SQLITE_BUSY_SNAPSHOT,需回滚重试。COMMIT落盘时机由日志模式决定。

🔎

延伸解读

三种 BEGIN 模式:取锁时机而非隔离级别

SQLite 的 DEFERRED、IMMEDIATE、EXCLUSIVE 并不改变隔离级别,事务始终是 SERIALIZABLE。它们只决定何时获取锁:DEFERRED 推迟到第一条语句,IMMEDIATE 在 BEGIN 时立即获取 RESERVED,EXCLUSIVE 在 WAL 模式下与 IMMEDIATE 等价,仅在回滚日志模式下额外阻止其他连接读取。理解这一点有助于避免将锁时机与隔离强度混淆。

单写者模型如何规避 write skew

SQLite 通过写串行化实现 SERIALIZABLE,同一时刻仅允许一个写事务,因此 write skew 依赖的多个并发写事务前提不成立。这不是检测并阻止了 write skew,而是从设计上杜绝了其出现。与 PostgreSQL 的 SSI 相比,SQLite 牺牲了写并发度换取了实现的简单性,两者是不同工程路径下的同等隔离保证。

SQLITE_BUSY_SNAPSHOT 与重试策略

在 WAL 模式下,若事务先读后写,而其他连接已提交新数据,则升级为写事务时会返回 SQLITE_BUSY_SNAPSHOT。此时简单重试无效,必须回滚并重新开始事务以获取最新快照。官方建议:若事务已知将写入,应使用 BEGIN IMMEDIATE 以避免此错误。区分 SQLITE_BUSY 与 SQLITE_BUSY_SNAPSHOT 对制定重试策略至关重要。

COMMIT 的落盘时机与失败重试

SQL 层的 COMMIT 命令并不直接写盘,它只是切换回自动提交模式,实际落盘由日志模式决定。若因其他连接持有共享锁而失败,COMMIT 会自动恢复事务状态,应用需检查返回码并重试。此外,关闭回滚日志(journal_mode=OFF)会使 ROLLBACK 行为未定义,应用不应依赖回滚能力。

Q&A

SQLite 的 BEGIN DEFERRED、IMMEDIATE、EXCLUSIVE 三种模式有什么区别?

三种模式的区别在于取锁时机:DEFERRED(默认)推迟到第一条语句才取锁,若首条是 SELECT 则只拿 SHARED,若是写语句则直接拿 RESERVED;IMMEDIATE 在 BEGIN 时立即尝试获取 RESERVED 锁;EXCLUSIVE 在 BEGIN 时也立即获取 RESERVED 锁,但在 rollback journal 模式下会阻止其他连接读,在 WAL 模式下与 IMMEDIATE 完全相同。

SQLite 如何实现 SERIALIZABLE 隔离级别?

SQLite 通过写串行化实现 SERIALIZABLE:同一时刻只允许一个写者,写事务完全串行执行,因此不会出现并发写冲突,从而避免了 write skew 等异常。

SQLITE_BUSY_SNAPSHOT 错误是什么?如何处理?

SQLITE_BUSY_SNAPSHOT 仅在 WAL 模式下出现,当一个事务先读后写,但读取的快照已被其他已提交事务超越时,尝试升级为写事务会返回该错误。处理方法是必须 ROLLBACK 当前事务并重新 BEGIN,以获取最新快照,单纯重试无效。

SQLite 的 WAL 模式下的快照隔离与 PostgreSQL 的 MVCC 有何不同?

两者在读者视角相似,都提供一致性快照,但写者侧不同:PostgreSQL 允许多个写事务并发,通过锁或 SSI 检测冲突;SQLite 全局单写者,写事务必须基于最新状态,否则返回 SQLITE_BUSY_SNAPSHOT,而不是基于旧快照写后再检测。

为什么在 SQLite 中 write skew 不会发生?

write skew 需要至少两个并发写事务基于各自旧快照做出冲突决定。SQLite 采用单写者模型,同一时刻只有一个写事务,因此 write skew 的前提不成立,它不是被检测阻止,而是从未出现。

在 SQLite 中,COMMIT 命令执行后数据一定落盘了吗?

不一定。COMMIT 命令本身只是将连接切回 autocommit 模式,真正的落盘发生在命令执行完毕后。如果此时其他连接仍持有 SHARED 锁,COMMIT 可能失败并自动回到未提交状态,应用需要检查返回码并重试。

SQLITE_BUSY 和 SQLITE_BUSY_SNAPSHOT 有什么区别?

SQLITE_BUSY 表示锁被其他连接占用,通常发生在 rollback journal 模式,重试可能成功;SQLITE_BUSY_SNAPSHOT 仅在 WAL 模式,表示当前事务的快照已过期,重试无效,必须回滚并重新开始事务。

如果事务先读后写,应该使用哪种 BEGIN 模式?

建议使用 BEGIN IMMEDIATE。因为 DEFERRED 模式先读后写时,可能在读和写之间被其他连接抢先提交,导致 SQLITE_BUSY 或 SQLITE_BUSY_SNAPSHOT 错误。BEGIN IMMEDIATE 在开始时即获取 RESERVED 锁,避免后续升级失败。

🏷️

标签

➡️

继续阅读