【SQLite 内核】ATTACH / 多库边界:跨库事务的原子性在 WAL 下会退化
内容提要
本文介绍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 涉及的所有文件由同一个进程内的同一个连接统一协调,不涉及网络或多个独立协调者,本质上仍是单进程本地事务的扩展,不是分布式事务。