【SQLite 内核】ATTACH / 多库边界:跨库事务的原子性在 WAL 下会退化

💡 原文中文,约11600字,阅读约需28分钟。
📝

内容提要

本文介绍SQLite的ATTACH DATABASE功能,允许一个连接同时操作多个数据库文件。跨库事务在rollback journal模式下通过super-journal机制保证原子性,但WAL模式下会退化为各文件独立原子。锁按文件独立跟踪,跨库写需在所有文件上获取EXCLUSIVE锁。事务进行中的库无法DETACH。同名表省略前缀时取最早挂载的库。

🔎

延伸解读

WAL 模式下的跨库原子性退化

官方文档明确指出,当主库或任一附加库使用 WAL 模式时,跨库事务的原子性会退化为每个文件各自原子,文件之间不再联合原子。这意味着在崩溃发生时,可能出现部分文件已提交、部分未提交的情况。这与 rollback journal 模式下通过 super-journal 实现的强原子性形成对比。应用若依赖跨库事务的强一致性,需避免在参与事务的库上启用 WAL,或自行在应用层处理不一致。

super-journal 的协调机制

多文件事务的原子性依赖于 super-journal 文件,它记录了所有参与库的 rollback journal 路径。提交的关键在于删除 super-journal 文件,这一动作同时判定所有文件的提交状态。该机制仅在 rollback journal 模式下有效,且 super-journal 的命名规则不属于规范,可能变化。理解这一机制有助于把握 SQLite 多文件提交的边界。

锁按文件独立跟踪的实践影响

每个附加库独立加锁,跨库写事务必须在所有参与文件上获取 EXCLUSIVE 锁后才能写入。这导致事务进行中的库无法被 DETACH,因为其持有写锁。实测中,尝试在事务中 DETACH 会报错 'database is locked'。这提醒开发者,DETACH 操作受锁状态约束,而非仅依赖文件存在。

同名表解析的隐式规则

当多个附加库存在同名表且省略 schema 前缀时,SQLite 不会报歧义错误,而是确定性地选择最早挂载的库中的表。这一行为依赖 ATTACH 的顺序,可能带来意外结果。生产代码中应始终使用 schema-name.table-name 的完整引用,避免依赖隐式规则。

Q&A

SQLite 的 ATTACH DATABASE 命令有什么作用?

ATTACH DATABASE 命令允许一个数据库连接同时打开多个独立的数据库文件,将它们加入当前连接的命名空间,从而可以跨文件执行 SELECT、JOIN 和事务。

在 SQLite 中,跨库事务在 WAL 模式下原子性会如何变化?

在 WAL 模式下,跨库事务的原子性会退化为每个文件各自原子,但文件之间不再联合原子。如果崩溃发生在提交过程中,某些文件可能已更新,而其他文件未更新。

SQLite 的 super-journal 机制是如何实现多文件事务原子性的?

super-journal 是一个额外的文件,记录了所有参与事务的数据库各自的 rollback journal 路径。在提交时,删除 super-journal 文件这一动作同时判定所有文件的提交状态,从而将多文件原子性收敛为单个文件的存在性判定。

在 SQLite 中,跨库写事务需要满足什么锁条件?

跨库写事务必须在所有参与文件上都获得 EXCLUSIVE 锁之后才能开始写入任何文件。每个数据库文件独立加锁,锁状态按文件分别跟踪。

为什么在事务进行中无法 DETACH 一个数据库?

因为事务进行中,参与事务的数据库文件持有写锁(至少 RESERVED 锁),而 DETACH 需要关闭该文件的连接句柄,SQLite 不允许在文件被锁定期间执行此操作,因此会报错 'database is locked'。

在 SQLite 中,如果多个附加数据库有同名表且省略前缀,会如何解析?

如果多个附加数据库中有同名表,且查询时省略了 schema 前缀,SQLite 会选择最早挂载(least recently attached)的那个数据库中的表。

SQLite 的 ATTACH 是否等同于分布式事务或两阶段提交?

不等同。ATTACH 涉及的所有文件由同一个进程内的同一个连接统一协调,不涉及网络或多个独立协调者,本质上仍是单进程本地事务的扩展,不是分布式事务。

🏷️

标签

➡️

继续阅读