【SQLite 内核】事务与隔离:DEFERRED/IMMEDIATE/EXCLUSIVE 与单写者快照
内容提要
本文介绍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 锁,避免后续升级失败。