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,你可以暴露可写的表,并通过它访问任何数据源。 但是,真正的事务完整性需要的不仅仅是行级别的更新,还需要事务边界的钩子函数:

但是,当你的 Virtual Table 与其他 Virtual Table 或者普通表一起被修改时会发生什么? SQLite 如何确保所有内容都原子地提交或回滚?

SQLite 事务的幕后机制

在处理 vtable 钩子之前,让我们回顾一下 SQLite 默认情况下如何处理事务。

回滚日志 (Rollback Journals)

在其最简单的模式下,SQLite 使用回滚日志。 在覆盖任何页面之前,它会将原始页面写入日志文件。 如果出现问题,SQLite 会从日志中恢复以保证原子性。

注意:SQLite 还支持 WAL 模式,但这超出了本文的范围。

用于多数据库的超级日志 (Super-Journals)

如果你附加了其他数据库,则每个文件的单个回滚日志无法协调它们之间的提交。 这时就需要超级日志:一个顶级日志文件,它跨越所有受影响的数据库,确保多文件提交保持原子性。

但是,对于同一数据库文件中的多个 Virtual Table,标准回滚日志就足够了;不需要超级日志。 在所有情况下——无论是一个文件中的多个 vtable 还是跨附加数据库—— Virtual Table 钩子 (xSyncxCommitxRollback) 都会作为 SQLite 事务过程的一部分被调用。

Virtual Tables 的两阶段提交

SQLite 的两阶段提交分解如下:

  1. 第一阶段 (xSync)
    • SQLite 将所有页面(或日志)写入并同步到磁盘,用于每个 B-tree 和附加的数据库。
    • 对于 Virtual Table,它会调用每个模块的 xSync 钩子。 如果任何 xSync 失败,则整个事务都会回滚——原子性得以保留。
  2. 第二阶段 (清理)
    • 一旦保证了持久性,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 会忽略来自 xCommitxRollback 的返回代码。 这些钩子应该只删除临时状态(日志、锁),并且不能执行可能失败的工作。

对 Virtual Table 作者的影响

现在,你应该了解 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.