SpacetimeDB 核心架构详解:Host、Database、Table、Reducer、Procedure 与 View 全解析

发布时间:2026/9/12 23:35:41
SpacetimeDB 核心架构详解:Host、Database、Table、Reducer、Procedure 与 View 全解析 SpacetimeDB 核心架构详解Host、Database、Table、Reducer、Procedure 与 View 全解析【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB本文以 SpacetimeDB 官方文档《Key Architecture》为骨架系统梳理其核心架构概念——从承载数据库运行的Host到定义 schema 与业务逻辑的Module再到存储数据的Table、处理写操作的Reducer、支持外部 I/O 的Procedure、只读计算的View以及围绕它们的Client、Identity、ConnectionId与Energy体系。读完本文你将理解 SpacetimeDB 各组件间的职责边界与协作关系掌握在 TypeScript、C#、Rust、C 四种语言中定义表、编写约减器reducer、过程procedure与视图view并完成客户端调用的完整实战方法并了解事务原子性、身份认证与实时订阅在底层是如何工作的。本文所述核心概念原文位于 docs/docs/00100-intro/00100-getting-started/00400-key-architecture.md文中所有实现细节均可结合仓库中的源码与配套文档交叉验证。Host承载数据库的服务器Host主机是托管Database数据库的服务器。你可以运行自己的 host也可以使用 SpacetimeDB 官方提供的托管云服务maincloud。一个 host 上可以同时运行多个数据库。从源码结构看host 侧的实现分布在仓库的多个核心 crate 中例如负责事务执行与状态管理的 crates/execution/src、负责数据库存储引擎的 crates/datastore/src以及负责订阅与实时推送的 crates/subscription/src。如需自建 host可以参考 docs/docs/00300-resources/00100-how-to/00100-deploy/00200-self-hosting.md 中的说明。Database运行在 Host 上的应用实例Database数据库是运行在 host 上的一个应用。它向外导出两类东西Table表用于存储数据Reducer约减器允许Client客户端发起请求。一个数据库的 schema 与业务逻辑由名为Module模块的软件定义。模块可以用 C#、C、Rust 或 TypeScript 编写。从技术上严格来说SpacetimeDB 模块是一个 WebAssembly 模块 中定义的低级 WebAssembly ABI并导出少量特殊函数。不过SpacetimeDB 的服务端库把这些底层细节都封装掉了——对开发者而言编写模块与编写普通应用几乎无异唯一的区别是部署时使用专用的 CLI 工具spacetime来完成。Module 与 Database 的区别关于 Module 与 Database 的区别数据库模块文档 给出了清晰的说明Module是你编写的代码定义 schema表与业务逻辑reducer、procedure、view编译后部署到 SpacetimeDBDatabase是模块的一个运行实例拥有模块的 schema 与逻辑外加真实存储的数据。同一个模块可以部署到多个数据库例如分别用于测试、预发、生产环境每个数据库拥有相互独立的数据。当你更新模块代码并重新发布时SpacetimeDB 会更新该数据库的 schema 与逻辑已有数据保持不变复杂 schema 变更可能需要谨慎处理迁移。数据库的命名与运维发布模块时你需要给数据库命名名称必须匹配正则/^[a-z0-9](-[a-z0-9])*$/即只允许小写 ASCII 字母和数字用短横线分隔。合法的示例包括my-game-server、chat-app-production、test123。每个数据库创建时还会获得一个唯一的身份标识hex 字符串客户端既可以用名称连接也可以用身份标识连接。日常运维通过spacetimeCLI 完成# 创建或更新数据库发布模块 spacetime publish DATABASE_NAME # 永久删除数据库及其全部数据--yes 可在脚本中跳过确认 spacetime delete DATABASE_NAME # 以 SQL 直接查询数据库作为数据库所有者可绕过表可见性限制 spacetime sql DATABASE_NAME SELECT * FROM user # 以匿名客户端身份查询遵守表可见性规则 spacetime sql --anonymous DATABASE_NAME SELECT * FROM user # 查看日志--follow 实时流式输出--num-lines 限制条数 spacetime logs DATABASE_NAME # 列出当前身份关联的所有数据库 spacetime listTableSQL 数据库表Table表本质上就是一张 SQL 数据库表在模块的原生语言中声明。表的内容可以被Reducer读取和更新标记为public的表还可以被Client直接读取。四种语言中的表声明语言声明方式TypeScripttable({ name, public }, { 列定义 })C#[SpacetimeDB.Table(Accessor Player, Public true)]partial structRust#[spacetimedb::table(accessor players, public)]structCSPACETIMEDB_STRUCTSPACETIMEDB_TABLEFIELD_PrimaryKey以一张包含自增主键、名字、年龄和用户身份的Player表为例四种语言的定义如下import { table, t } from spacetimedb/server; const players table( { name: players, public: true }, { id: t.u64().primaryKey(), name: t.string(), age: t.u32(), user: t.identity(), } );[SpacetimeDB.Table(Accessor Player, Public true)] public partial struct Player { [SpacetimeDB.PrimaryKey] uint playerId; string name; uint age; Identity user; }#[spacetimedb::table(accessor players, public)] pub struct Player { #[primary_key] id: u64, name: String, age: u32, user: Identity, }struct Player { uint64_t id; std::string name; uint32_t age; Identity user; }; SPACETIMEDB_STRUCT(Player, id, name, age, user) SPACETIMEDB_TABLE(Player, players, Public) FIELD_PrimaryKey(players, id)从仓库的真实模块代码可以看出同样的模式。例如 templates/basic-rs/spacetimedb/src/lib.rs 中的最小 Rust 模块use spacetimedb::{ReducerContext, Table}; #[spacetimedb::table(accessor person, public)] pub struct Person { name: String, }在 Rust 中insert、try_insert、iter、count等表操作由Tabletrait 提供需要显式导入use spacetimedb::Table;。表还可以配合主键、唯一约束、索引btree/hash、自动递增等能力使用详见 列类型、索引 与 约束 等文档。Reducer数据库导出的远程过程调用入口Reducer约减器是数据库导出的一种函数。已连接的Client可以调用 reducer 来与数据库交互这本质上是一种远程过程调用RPC。服务端定义与客户端调用TypeScript 模块中定义 reducerexport const setPlayerName spacetimedb.reducer({ id: t.u64(), name: t.string() }, (ctx, { id, name }) { // ... });TypeScript 客户端调用function main() { // ...setup code, then... ctx.reducers.setPlayerName(57n, Marceline); }C# 模块中定义[SpacetimeDB.Reducer] public static void SetPlayerName(ReducerContext ctx, uint playerId, string name) { // ... }C# 客户端调用void Main() { // ...setup code, then... Connection.Reducer.SetPlayerName(57, Marceline); }Rust 模块中定义#[spacetimedb::reducer] pub fn set_player_name(ctx: spacetimedb::ReducerContext, id: u64, name: String) - Result(), String { // ... }Rust 客户端调用fn main() { // ...setup code, then... ctx.reducers.set_player_name(57, Marceline.into()); }C 模块中定义SPACETIMEDB_REDUCER(set_player_name, ReducerContext ctx, uint64_t id, std::string name) { // ... return Ok(); }Unreal C 客户端调用void AMyGameManager::UpdatePlayerName() { // ...setup code, then... Conn-Reducers-SetPlayerName(57, Marceline); }这些调用看起来与普通函数调用无异但底层是客户端通过互联网发送请求由数据库处理后返回响应。ReducerContext 与调用方身份ReducerContext是 reducer 唯一必选的参数其中包含调用者 Identity 的信息可用于对调用者做鉴权。在 reducer context 文档 中可以看到更完整的上下文能力说明。事务性全有或全无每个 reducer 都在自己独立的原子数据库事务中运行当 reducer 成功完成时它所做的一切变更例如插入一行会被commit提交到数据库当 reducer 返回错误或抛出异常时数据库会**拒绝该请求并回滚revert**所有变更。也就是说reducer 与事务是全有或全无的请求不可能保留 reducer 前半部分变更而丢弃后半部分。不支持嵌套事务事务只能由数据库外部的请求发起。当一个 reducer 直接调用另一个 reducer如下例被调用 reducer 的变更不会运行在独立的子事务中即使被嵌套调用的 reducer 优雅地出错只要整体 reducer 成功完成嵌套 reducer 的变更依然会被持久化。#[spacetimedb::reducer] pub fn hello(ctx: spacetimedb::ReducerContext) - Result(), String { if world(ctx).is_err() { other_changes(ctx); } } #[spacetimedb::reducer] pub fn world(ctx: spacetimedb::ReducerContext) - Result(), String { clear_all_tables(ctx); }TypeScript 与 C# 的对应写法export const hello spacetimedb.reducer((ctx) { try { world(ctx); } catch { otherChanges(ctx); } }); export const world spacetimedb.reducer((ctx) { clearAllTables(ctx); // ... });[SpacetimeDB.Reducer] public static void Hello(ReducerContext ctx) { if(!World(ctx)) { OtherChanges(ctx); } } [SpacetimeDB.Reducer] public static void World(ReducerContext ctx) { ClearAllTables(ctx); // ... }SPACETIMEDB_REDUCER(world, ReducerContext ctx) { clear_all_tables(ctx); return Ok(); } SPACETIMEDB_REDUCER(hello, ReducerContext ctx) { if (world(ctx).is_err()) { other_changes(ctx); } return Ok(); }虽然 SpacetimeDB 不支持嵌套事务但 reducer 可以通过调度表schedule tables来调度另一个 reducer 按固定间隔或在指定时间运行Rust 侧亦可参考 docs.rs 上 spacetimedb 的 scheduled reducers 文档。关于 reducer 的更多细节ACID 保证、嵌套调用、最佳实践、全局变量陷阱等可参阅 Reducers 完整文档 与事务与原子性。值得特别注意的是reducer 是修改数据库状态的唯一途径所有数据库变更都必须经过 reducer同时 reducer 运行在隔离环境中不能发起网络请求、访问文件系统或执行系统调用这类能力属于下一节的过程procedure。Procedure支持外部 I/O 的数据库函数Procedure过程是数据库导出的一种函数与 reducer 类似已连接的客户端可以调用它。procedure 能执行 reducer 中无法完成的操作包括向外部服务发起 HTTP 请求。但 procedure 不会自动运行在数据库事务中必须手动开启并提交事务才能读取或修改数据库状态。因此除非确实需要 procedure 的特殊能力否则优先使用 reducer。各语言定义与调用TypeScriptexport const makeRequest spacetimedb.procedure(t.string(), ctx { // ... })客户端调用并注册完成回调接收返回值ctx.procedures.makeRequest().then( res console.log(Procedure make_request returned ${res}), err console.error(Procedure make_request failed! ${err}), );C#[SpacetimeDB.Procedure] public static string MakeRequest(ProcedureContext ctx) { // ... return result; }客户端调用与回调C# 中 procedure 属于 unstable 特性需要在文件顶部加#pragma warning disable STDB_UNSTABLEctx.Procedures.MakeRequestThen((ctx, res) { if (res.IsSuccess) { Log.Debug($Procedure make_request returned {res.Value!}); } else { throw new Exception($Procedure make_request failed: {res.Error!}); } });Rust因为 procedure 尚不稳定Rust 模块需要在Cargo.toml中显式开启unstablefeature[dependencies] spacetimedb { version 2.*, features [unstable] }#[spacetimedb::procedure] pub fn make_request(ctx: mut spacetimedb::ProcedureContext) - String { // ... }Rust 客户端调用与回调ctx.procedures.make_request_then(|ctx, res| { match res { Ok(string) log::info!(Procedure make_request returned {string}), Err(e) log::error!(Procedure make_request failed! {e:?}), } })CSPACETIMEDB_PROCEDURE(std::string, make_request, ProcedureContext ctx) { // ... return std::string{result}; }procedure 与 reducer 的关键差异维度ReducerProcedure事务自动运行在独立原子事务中不自动开启事务需手动with_tx/WithTx开启并提交外部 I/O禁止无网络、无文件系统支持 HTTP 请求外部服务返回值广播给订阅者仅发送给调用者不广播给其他客户端典型场景一切数据库写操作HTTP 集成、外部服务交互关于 procedure 更完整的说明手动事务、try_with_tx失败处理、从事务中读出值、HTTP 请求与 30 秒默认超时/180 秒上限等见 Procedures 文档。此外reducer 无法直接调用 procedureprocedure 可能产生与事务执行不兼容的副作用而是通过往调度表插入记录来调度 procedure 在指定时间执行。View只读的计算查询View视图是数据库导出的一种只读函数它基于表计算并返回结果。与 reducer 不同view不会修改数据库状态只负责查询并返回数据。view 非常适合在把结果发送给客户端之前先在服务端完成派生数据计算、聚合或多表连接。View 必须声明为public并且只接受一个上下文参数。它可以返回单行或多行。与表一样view 可以被订阅并在其底层数据变化时自动更新。各语言定义TypeScriptexport const myPlayer spacetimedb.view( { name: my_player, public: true }, t.option(players.rowType), (ctx) { const row ctx.db.players.identity.find(ctx.sender); return row ?? undefined; } );C#[SpacetimeDB.View(Accessor MyPlayer, Public true)] public static Player? MyPlayer(ViewContext ctx) { return ctx.Db.Player.Identity.Find(ctx.Sender) as Player; }Rust#[spacetimedb::view(accessor my_player, public)] fn my_player(ctx: spacetimedb::ViewContext) - OptionPlayer { ctx.db.player().identity().find(ctx.sender()) }CSPACETIMEDB_VIEW(std::optionalPlayer, my_player, Public, ViewContext ctx) { return ctx.db[player_identity].find(ctx.sender()); }用 SQL 查询与订阅 ViewSELECT * FROM my_player;ViewContext 与 AnonymousViewContext 的性能差异View 使用两种上下文之一选择对性能影响显著详见 Views 文档ViewContext通过ctx.sender暴露调用者的Identity适合视图结果依赖查询者身份的场景如我的背包、我的消息AnonymousViewContext不提供调用者信息适合所有订阅者结果相同的场景如全局排行榜、商店库存、世界地图区域。匿名视图AnonymousViewContext可以在所有订阅者之间共享SpacetimeDB 知道结果对每个客户端都相同因此只物化一次并广播给所有人而按用户视图ViewContext必须为每个订阅者单独计算——若有 1000 个在线用户就需要 1000 次独立计算与变更跟踪。设计时应尽可能使用AnonymousViewContext例如把我附近的实体改造成区域 X 的实体让同一区域的玩家共享同一份物化结果。View 的索引访问约束与 Query BuilderView 只能通过索引查找find()、filter()和表级元数据查询如count()访问数据不能使用.iter()全表扫描。原因在于 view 函数是黑盒图灵完备代码SpacetimeDB 无法静态分析当 view 使用.iter()扫描整张表时其读集包含表中每一行任何一行变化都会触发整表重算而索引查找能让数据库精确定位依赖行实现定向失效保持更新快速可预测。如果视图逻辑主要是过滤与连接官方推荐使用模块端 Query Builderctx.from.players.where(...)/ctx.From.Player().Where(...)把工作下推到查询引擎——查询引擎可以进行全局优化、增量求值无需整段重算并避免在 WASM/V8 边界反复物化行数据。对于 join 密集型视图这一差异往往非常显著。Client连接到数据库的应用Client客户端是连接到数据库的应用。客户端使用Identity登录并获得一个ConnectionId来标识该连接。此后它可以调用Reducer并查询公开的Table。客户端使用客户端侧 SDK编写。spacetimeCLI 工具可以通过spacetime generate自动生成与客户端 SDK 配套的类型安全绑定代码从而与特定数据库通信。客户端 SDK 内部维护一条到 SpacetimeDB 的长连接流式通信WebSocket支撑高性能的实时交互。客户端是普通软件应用开发者可以自由选择部署方式Steam、应用商店、包管理器或其他方式。客户端 SDK 与本地缓存各语言 SDKRust、C#、TypeScript、Unreal C/Blueprint功能保持一致切换语言主要只是语法差异。客户端通过订阅subscriptions在本地维护一份数据库行的缓存订阅数据变化时自动同步。客户端 SDK 提供以下回调订阅更新订阅查询被应用或失败时行变更本地缓存中的行被插入、更新、删除时Reducer 调用服务端 reducer 运行时Procedure 结果procedure 调用完成时通过回调返回结果。客户端连接与 SDK 使用细节可参阅 连接文档 与 SDK API 文档。Identity跨连接的全局用户身份Identity标识与数据库交互的某个用户。它是一个长期有效、公开、全局唯一的标识符即使跨越不同连接也始终指向同一个终端用户。用户的Identity会被附加到他们发起的每一次 reducer 调用上你可以据此决定允许他们做什么鉴权模块本身也有 Identity当你执行spacetime publish发布模块时系统会自动为它签发一个 Identity用于与其他模块区分。你的客户端应用连接 host 时需要提供该 Identity。Identity 的签发机制Identity 依据OpenID Connect规范签发。数据库开发者负责给自己的终端用户签发 Identity。OpenID Connect 让用户可以通过 Google、Facebook 等标准服务登录这些账户。具体而言Identity 由 JSON Web Token (JWT) 的 issuer签发方与 subject主体字段哈希推导而来伪代码如下def identity_from_claims(issuer: str, subject: str) - [u8; 32]: hash1: [u8; 32] blake3_hash(issuer | subject) id_hash: [u8; 26] hash1[:26] checksum_hash: [u8; 32] blake3_hash([ 0xC2, 0x00, *id_hash ]) identity_big_endian_bytes: [u8; 32] [ 0xC2, 0x00, *checksum_hash[:4], *id_hash ] return identity_big_endian_bytes你可以从 SpacetimeDB 的开箱即用身份提供者 SpacetimeAuth 获取 JWT也可以从任何符合 OpenID Connect 规范的第三方身份提供者获取。ConnectionId标识单条客户端连接ConnectionId标识客户端到 SpacetimeDB 数据库的一条连接。一个用户只有一个Identity但可能同时打开多条连接——每条连接都会获得一个唯一的ConnectionId。Energy支付存储与计算成本的通证Energy能量是在 SpacetimeDB host 中用于支付数据存储和计算操作成本的货币。注意该章节在官方文档中标注为待完善TODO(1.0): Rewrite this section after finalizing energy SKUs.具体计价 SKU 尚未定型实际计费规则以 host 侧最终实现为准。全链路协作一次调用背后的完整流程综合上述概念一次典型的 SpacetimeDB 交互流程如下模块开发与发布用 Rust/C#/C/TypeScript 编写模块表 reducer procedure view通过spacetime publish发布到 host 上的某个数据库模块自动获得自己的 Identity客户端连接与鉴权客户端用 Identity来自 JWT登录数据库获得 ConnectionId并建立长连接流式通信订阅实时数据客户端用 SQL 或类型化查询构建器订阅公开表与 viewhost 在提交事务后以TransactionUpdate消息推送变更SDK 原子地更新本地缓存并触发回调订阅语义详见 docs/docs/00200-core-concepts/00400-subscriptions/00200-subscription-semantics.md调用 reducer客户端发起远程过程调用reducer 在独立原子事务中执行——成功则提交并广播变更失败则整体回滚调用 procedure需要外部 I/O 时客户端调用 procedure由其在事务外执行 HTTP 请求再通过with_tx手动提交数据库变更结果仅返回给调用者只读计算客户端按需查询或订阅 view服务端基于索引读集做定向失效与增量更新。延伸阅读数据库模块Module 与 Database 详解Reducers 完整参考Procedures 完整参考Views 完整参考含性能指南与 Query Builder事务与原子性客户端 SDK 概览订阅语义Module ABI 参考底层 WebAssembly ABI真实模块示例templates/basic-rs/spacetimedb/src/lib.rsRust、templates/basic-ts/spacetimedbTypeScript、templates/basic-cs/spacetimedbC#、templates/basic-cpp/spacetimedbC【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考