ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpacetimeDB 事务与原子性深度指南:Reducer 自动事务、ACID 保证与手动事务管理

SpacetimeDB 事务与原子性深度指南:Reducer 自动事务、ACID 保证与手动事务管理 SpacetimeDB 事务与原子性深度指南Reducer 自动事务、ACID 保证与手动事务管理【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB本篇指南围绕 SpacetimeDB 的核心事务模型展开所有 reducer 调用都在一个数据库事务中执行要么整体提交、要么整体回滚配合原子性、一致性、隔离性、持久性ACID四大保证让多人在线应用在高并发写入下依然保持数据一致。读完本文你将掌握 reducer 自动事务的边界、嵌套 reducer 调用如何共享事务、procedure 中with_tx/withTx手动事务的用法以及保持事务短小、处理错误回滚、规避auto_inc序列空洞等实战要点。什么是事务在 SpacetimeDB 中事务transaction是一组作为单个不可分割工作单元执行的数据库操作序列。每次 reducer 调用就是一个事务——要么它对数据库的全部更改成功提交要么全部回滚就像该 reducer 从未运行过一样。从源码角度看这个保证由数据库引擎层落地。RelationalDB提供了完整的 begin / commit / rollback 原语例如 crates/engine/src/relational_db.rs 中的begin_mut_tx开启一个可变事务、同文件 L848 的begin_tx只读事务、L856 的rollback_mut_tx回滚与 L875 的commit_tx提交。reducer 执行时模块宿主module host会在调用前后自动走完开启事务 → 执行函数 → 按结果提交或回滚的完整流程模块作者无需也不应手动干预。ACID 性质SpacetimeDB 事务提供以下四类保证原子性Atomicity全有或全无一个 reducer 的更改要么全部成功、要么全部失败不存在部分状态如果 reducer 成功完成所有更改插入、更新、删除都会提交到数据库如果 reducer 抛出异常或返回错误所有更改都会回滚数据库保持原样不可能在一次 reducer 执行中保留部分更改、丢弃其余部分。RelationalDB::finish_tx正是这一逻辑的直接实现crates/engine/src/relational_db.rs传入的函数执行结果res为Err时调用rollback_mut_tx回滚并上报指标为Ok时调用commit_tx提交。与之配套的with_auto_commitL989把开启事务 → 执行闭包 → 根据结果提交/回滚封装成一个原子操作是引擎内部绝大多数写操作的标准入口。注意该原子性保证针对reducer在单个事务中运行。Procedure存储过程可以手动开启多个独立事务其中每个事务各自原子但 procedure 整体不具备原子性。详见下文 Procedure手动事务。一致性Consistency只允许有效状态事务确保数据库从一个有效状态转移到另一个有效状态。所有约束唯一键、索引、以及业务逻辑中的外键式关系都会被强制执行唯一约束在提交前检查如果某个约束将被违反整个事务回滚数据库永远不会进入无效状态。隔离性Isolation一致的快照视图每个 reducer 看到的都是一致的数据库快照不会观察到其他 reducer 执行的中间更改reducer 看到的是其事务开始时的数据库状态reducer 运行期间不会观察到其他 reducer 对数据库的修改每个 reducer 完整执行完毕后其更改才对其他 reducer 可见。这避免了竞态条件并保证了行为可预测。源码层面RelationalDB::begin_mut_tx接受显式的IsolationLevel参数而引擎内部默认以IsolationLevel::Serializable可串行化最强隔离级别开启事务见 with_auto_commit 实现这从实现上支撑了一致快照、互不可见中间状态的语义。持久性Durability提交即持久一旦 reducer 成功提交其更改就是永久的并且在服务器重启后依然存在。SpacetimeDB 会将已提交的事务持久化到磁盘。这一保证的落点之一是Durability子系统提交路径上crates/engine/src/durability.rs 的request_durability会把事务数据插入、删除、截断等变更操作组装为Transaction追加到持久化队列在 crates/durability/src/imp/local.rs 中append_tx将事务写入 commit log 队列并由后台 actor 同步到磁盘同时维护durable_offset已持久化的事务偏移。这也解释了为什么提交后重启数据仍在每次提交都会在落盘后才视为持久化完成。事务作用域Reducer自动事务每次 reducer 调用都会自动运行在自己的事务中事务在 reducer 被调用时开始事务在 reducer 成功返回时提交事务在 reducer 抛出异常或返回错误时回滚。你不需要在 reducer 中手动开启或提交事务——SpacetimeDB 自动处理这一切。嵌套 reducer 调用当一个 reducer 直接调用另一个 reducer而非通过调度时它们会在同一个事务中执行。四种服务端语言的表现一致TypeScriptexport const parentReducer spacetimedb.reducer((ctx) { TableA.insert({ /* ... */ }); // 这运行在 SAME 事务中 childReducer(ctx); TableB.insert({ /* ... */ }); // 父与子产生的所有更改一起提交 }); function childReducer(ctx) { TableC.insert({ /* ... */ }); // 如果这里抛错父 reducer 的更改也会一起回滚 if (someCondition) { throw new Error(Child failed); } }C#[SpacetimeDB.Reducer] public static void ParentReducer(ReducerContext ctx) { ctx.Db.TableA.Insert(new RowA { /* ... */ }); // 这运行在 SAME 事务中 ChildReducer(ctx); ctx.Db.TableB.Insert(new RowB { /* ... */ }); // 父与子产生的所有更改一起提交 } [SpacetimeDB.Reducer] public static void ChildReducer(ReducerContext ctx) { ctx.Db.TableC.Insert(new RowC { /* ... */ }); // 如果这里抛异常父 reducer 的更改也会一起回滚 if (someCondition) { throw new Exception(Child failed); } }Rust#[reducer] pub fn parent_reducer(ctx: ReducerContext) - Result(), String { ctx.db.table_a().insert(RowA { /* ... */ }); // 这运行在 SAME 事务中 child_reducer(ctx)?; ctx.db.table_b().insert(RowB { /* ... */ }); // 父与子产生的所有更改一起提交 Ok(()) } #[reducer] pub fn child_reducer(ctx: ReducerContext) - Result(), String { ctx.db.table_c().insert(RowC { /* ... */ }); // 如果这里返回 Err父 reducer 的更改也会一起回滚 if some_condition { return Err(Child failed.to_string()); } Ok(()) }Cusing namespace SpacetimeDB; // 前向声明子 reducer允许在其定义之前调用 ReducerResult child_reducer(ReducerContext, bool some_condition); SPACETIMEDB_REDUCER(parent_reducer, ReducerContext ctx, bool some_condition) { ctx.db[table_a].insert(RowA{ /* ... */ }); // 这运行在 SAME 事务中 ReducerResult result child_reducer(ctx, some_condition); if (result.is_err()) { return result; } ctx.db[table_b].insert(RowB{ /* ... */ }); // 父与子产生的所有更改一起提交 return Ok(); } SPACETIMEDB_REDUCER(child_reducer, ReducerContext ctx, bool some_condition) { ctx.db[table_c].insert(RowC{ /* ... */ }); // 如果这里返回 Err父 reducer 的更改也会一起回滚 if (some_condition) { return Err(Child failed); } return Ok(); }重要SpacetimeDB不支持嵌套事务。嵌套 reducer 调用与父 reducer 在同一事务中执行。如果需要相互独立的事务应改用调度表schedule tables来调度 reducer。从实现上看reducer 事务由模块宿主的执行路径统一管理见 crates/core/src/host/module_host.rs子 reducer 调用只是父执行流程内的一个普通函数调用天然处于同一个事务上下文中因此原子性会一路向上传递。Procedure手动事务与 reducer 不同procedure不会自动在事务中运行。Procedure 可以执行事务但必须使用with_txRust / C或withTxTypeScript/WithTxC#手动开启。这给 procedure 带来更大的灵活性procedure 可以在事务之外执行操作例如 HTTP 请求等外部 I/Oprocedure 可以按需开启多个相互独立的事务每次with_tx/withTx调用都会创建一个独立提交的新事务。以 TypeScript 为例procedure 中没有ctx.db直接访问数据库必须显式包裹事务const myTable table( { name: my_table }, { a: t.u32(), b: t.string(), }, ) const spacetimedb schema({ myTable }); export default spacetimedb; export const insertAValue spacetimedb.procedure({ a: t.u32(), b: t.u32() }, t.unit(), (ctx, { a, b }) { ctx.withTx(ctx { ctx.db.myTable.insert({ a, b }); }); return {}; })ProcedureCtx.withTx接收一个(ctx: TransactionCtx) T的函数。在该函数内部TransactionCtx可以像ReducerCtx一样访问数据库参见 ReducerContext 文档。当函数返回时事务提交其对数据库状态的更改变为永久并对客户端广播如果函数抛出错误事务回滚更改被丢弃。Rust 侧对应ProcedureContext::with_tx接收Fn(TxContext) - T的闭包#[spacetimedb::procedure] fn do_work(ctx: mut spacetimedb::ProcedureContext, /* ... */) - /* ... */ { // 事务外的操作如 HTTP 请求 ctx.with_tx(|tx| { // tx.db 上的数据库读写 }); // ... }C 侧对应ctx.with_tx接收[](TxContext tx) - T的 lambda。对于可能失败的数据库操作三种语言还分别提供了try_with_txRust闭包返回ResultT, E、ctx.try_with_txC返回bool等变体以便在不 panic 的前提下显式控制提交/回滚。关于重复执行的注意点withTx/with_tx传入的函数可能被多次调用每次可能看到不同版本的数据库状态。因此若针对同一数据库状态被多次调用每次必须执行相同操作并返回相同结果若针对不同数据库状态被调用前一次运行观察到的值不得影响本次函数行为或整个 procedure 的行为不要在传入withTx/with_tx的函数中捕获可变状态。更多细节可参考 Procedures 文档。C# 与 C 的 procedure 目前属于不稳定特性使用时需分别添加#pragma warning disable STDB_UNSTABLE或#define SPACETIMEDB_UNSTABLE_FEATURES。最佳实践保持事务短小在 reducer 中只执行必要的数据库操作将外部 I/OHTTP 请求等移到 procedure 中更短的事务能减少争用contention并提高吞吐量。这一建议与引擎的事务管理方式一致begin_mut_tx需要与commit_tx/rollback_mut_tx成对使用事务持有期间数据库处于锁定的可变事务状态crates/engine/src/relational_db.rs因此事务持续时间越长并发写入被阻塞的概率越高。短事务 可串行化隔离级别才能让高并发场景稳定运行。优雅处理错误返回描述性错误帮助客户端理解失败原因Rust 中使用Result(), String其他语言抛出带清晰信息的异常记住任何错误都会回滚全部更改。限制不支持嵌套事务SpacetimeDB 不支持嵌套事务。当一个 reducer 调用另一个 reducer 时它们共享同一个事务。如果需要相互独立的事务请使用调度表异步触发第二个 reducer。调度表通过在表中加入scheduledAt等调度列让表在特定时间自动触发 reducer 或 procedureTypeScript 侧通过 reducer 的onSchedule选项绑定。由于被调度的 reducer 会在独立的、新的事务中执行调度器见 crates/core/src/host/scheduler.rs其中以self.db.begin_tx(Workload::Internal)等入口开启自己的事务因此它是把一个大事务拆成多个独立原子单位的推荐手段。自增序列不参与事务#[auto_inc]序列生成器不具备事务性即使事务回滚序列号也已被分配这会在序列中产生空洞gap详见 Auto-Increment 文档中的 Crash Recovery 一节。原因在于序列号分配与事务提交是两个独立过程为避免每次自增都落盘序列按4096个一组的批量方式分配边界并持久化因此回滚事务不会退还已经拿到的序号重启恢复时也会从下一个分配边界继续宁可跳过未使用值也绝不重复赋值。这在分布式/持久化场景下保证了永不重复代价就是序列中的空洞不可避免。相关主题Reducers以事务方式修改数据库状态的函数Procedures支持手动事务控制的函数调度表 Schedule Tables调度 reducer 以获取独立事务Auto-Increment自增列与序列崩溃恢复机制约束 Constraints唯一键等一致性约束在提交前的强制检查订阅 Subscriptions客户端如何接收事务性更新。【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表