SQLite 事务和 Virtual Tables 深入解析
Misframe
by Preetam Jinka About Categories Tags Apr 17, 2025
SQLite 事务和 Virtual Tables
Programming database sql sqlite go 在我之前的文章中,我介绍了 SQL 的 Virtual Tables 以及如何在 Go 中使用它们,包括如何为每个连接注册模块、定义模式以及像查询常规表一样查询外部数据源。
现在,让我们深入研究更高级的 Virtual Table 实现:那些支持写入和完整事务行为的实现。
Virtual Tables 中的写入和事务支持
SQLite 的 Virtual Table 接口并非只读。通过实现 xUpdate
,你可以暴露可写的表,并通过它访问任何数据源。 但是,真正的事务完整性需要的不仅仅是行级别的更新,还需要事务边界的钩子函数:
xBegin
: 标志事务的开始。xSync
: 准备持久化提交的工作;此处的失败将中止一切。xCommit
: 最终确定事务(仅清理)。xRollback
: 如果事务中止,则恢复更改。
但是,当你的 Virtual Table 与其他 Virtual Table 或者普通表一起被修改时会发生什么? SQLite 如何确保所有内容都原子地提交或回滚?
SQLite 事务的幕后机制
在处理 vtable 钩子之前,让我们回顾一下 SQLite 默认情况下如何处理事务。
回滚日志 (Rollback Journals)
在其最简单的模式下,SQLite 使用回滚日志。 在覆盖任何页面之前,它会将原始页面写入日志文件。 如果出现问题,SQLite 会从日志中恢复以保证原子性。
注意:SQLite 还支持 WAL 模式,但这超出了本文的范围。
用于多数据库的超级日志 (Super-Journals)
如果你附加了其他数据库,则每个文件的单个回滚日志无法协调它们之间的提交。 这时就需要超级日志:一个顶级日志文件,它跨越所有受影响的数据库,确保多文件提交保持原子性。
但是,对于同一数据库文件中的多个 Virtual Table,标准回滚日志就足够了;不需要超级日志。 在所有情况下——无论是一个文件中的多个 vtable 还是跨附加数据库—— Virtual Table 钩子 (xSync
、xCommit
、xRollback
) 都会作为 SQLite 事务过程的一部分被调用。
Virtual Tables 的两阶段提交
SQLite 的两阶段提交分解如下:
- 第一阶段 (
xSync
)- SQLite 将所有页面(或日志)写入并同步到磁盘,用于每个 B-tree 和附加的数据库。
- 对于 Virtual Table,它会调用每个模块的
xSync
钩子。 如果任何xSync
失败,则整个事务都会回滚——原子性得以保留。
- 第二阶段 (清理)
- 一旦保证了持久性,SQLite 就会清理日志文件并完成提交。
下面是来自 vdbeaux.c
的第二阶段逻辑的核心。 请注意,错误会被故意忽略——这纯粹是清理:
/* All files and directories have already been synced, so the following
** calls to sqlite3BtreeCommitPhaseTwo() are only closing files and
** deleting or truncating journals. If something goes wrong while
** this is happening we don't really care. The integrity of the
** transaction is already guaranteed, but some stray 'cold' journals
** may be lying around. Returning an error code won't help matters.
*/
disable_simulated_io_errors();
sqlite3BeginBenignMalloc();
for(i=0; i<db->nDb; i++){
Btree *pBt = db->aDb[i].pBt;
if( pBt ){
sqlite3BtreeCommitPhaseTwo(pBt, 1);
}
}
sqlite3EndBenignMalloc();
enable_simulated_io_errors();
/* Now perform virtual-table cleanup */
sqlite3VtabCommit(db);
在 vtab.c
中,Virtual Table 的提交钩子也同样被视为尽力而为:
/* From vtab.c: errors in xCommit are ignored—this is purely cleanup. */
int sqlite3VtabCommit(sqlite3 *db){
callFinaliser(db, offsetof(sqlite3_module,xCommit));
return SQLITE_OK;
}
因为 xSync
已经确保所有数据都安全地保存在磁盘上,所以 SQLite 会忽略来自 xCommit
和 xRollback
的返回代码。 这些钩子应该只删除临时状态(日志、锁),并且不能执行可能失败的工作。
对 Virtual Table 作者的影响
- 持久性必须放在
xSync
中。 任何可能失败的事情——网络 I/O、磁盘写入——都属于xSync
,因此此处的错误会中止事务。 xRollback
仍然可能在xSync
之后被调用。 即使xSync
需要处理持久性,如果另一个xSync
失败,操作可能仍然需要回滚。- 保持
xCommit
和xRollback
幂等。 仅执行清理;避免任何可能失败的操作。
现在,你应该了解 SQLite 的回滚和超级日志机制如何协调原子提交,以及你的 Virtual Table 如何挂钩到该两阶段过程中,以确保内置表和自定义表之间的一致性。
接下来阅读这些:
Mar 16, 2025
The 3 Stages of Startup Enlightenment
Aug 3, 2017
Comparing high availability for Google Cloud SQL and Amazon RDS ⭐️
Hi 👋. I’m a software engineer in the San Francisco Bay Area, and co-founder of FunnelStory.
Follow me on Twitter, GitHub, or LinkedIn.
Copyright © 2011-2025 Preetam Jinka.