尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SpacetimeDB Tables 全面指南:内存数据库的表设计、定义与访问控制

SpacetimeDB Tables 全面指南:内存数据库的表设计、定义与访问控制 SpacetimeDB Tables 全面指南内存数据库的表设计、定义与访问控制【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDBSpacetimeDB 以表Table作为全部数据的存储与组织单元——所有数据常驻内存以获得极低延迟与高吞吐同时自动持久化到磁盘保证可靠性。本指南围绕 SpacetimeDB 1.12.0 的表系统展开覆盖表的核心设计哲学数据导向设计、物理与逻辑独立性、表分解、TypeScript/C#/Rust 三种服务端语言中表的定义与访问器命名规则、公开/私有可见性、同一类型的多表模式、约束与自增列并结合仓库源码如系统表实现crates/datastore/src/system_tables.rs剖析底层原理。读完本文你将能够设计出高性能的 SpacetimeDB 表结构并在模块代码中正确声明、查询与组织数据。Tables 是什么内存优先、自动持久化的数据存储单元Tables 是 SpacetimeDB 中存储数据的唯一方式。其核心架构决策有两点数据常驻内存所有表数据保存在内存中为读写提供极低延迟与极高吞吐自动持久化SpacetimeDB 会在后台自动将所有数据持久化到磁盘重启后数据不丢失无需开发者手动管理持久化逻辑。这一内存 自动落盘的组合是 SpacetimeDB 面向实时、高频交互场景如多人游戏、实时协作的基石查询路径上享受内存访问的速度可靠性上又具备数据库级别的持久化保证。Why Tables表作为数据组织的基本单元表在 SpacetimeDB 中的地位正如文件在 Unix 中的地位——都是系统中最基本的数据组织单位。但表比文件更具一般性Unix 需要用独立的文件系统filesystem概念来组织和描述文件SpacetimeDB 则自我描述表和表结构的表示本身也存储在表中这些表被称为系统表system tables例如st_table与st_column。直接查询系统表系统表完全开放查询你可以用 SQL 直接检视当前数据库的元数据SELECT * FROM st_table; SELECT * FROM st_column;:::warning 你可以查询系统表但不应直接修改它们。所有 schema 变更都应通过模块代码中的正常定义机制完成绕开系统表的写入可能导致数据库状态不一致。 :::从源码层面看系统表在crates/datastore/src/system_tables.rs中被定义为一个固定 ID 的集合例如ST_TABLE_ID TableId(1)即st_table存储所有表的元数据ST_COLUMN_ID TableId(2)即st_column存储所有列的元数据后续还有st_sequence自增序列、st_index索引、st_constraint约束、st_module、st_client、st_var、st_scheduled调度表等共 20 余张系统表覆盖了表结构、序列、索引、约束、连接客户端、系统变量、定时调度等全部元信息。系统表的 ID 范围被保留用户创建的表从保留范围之后开始分配这也印证了系统表是一等公民、Schema 即数据的设计。表与数据导向设计Data-Oriented Design关系模型是数据导向设计的逻辑终点。常见的实体组件系统ECS模式实际上只是关系模型能力的一个严格子集而表赋予你完整的关系理论能力——这是五十多年来被反复验证的数据组织与查询技术。数据导向设计的核心原则是任何程序的目的都是将数据从一种形式转换为另一种形式。表为数据提供了有原则的、通用的表示并由此带来四项关键能力高效的访问模式通过索引indexes实现数据完整性通过约束constraints实现灵活的查询通过关系运算relational operations实现实时同步通过订阅subscriptions实现。关于这一设计哲学的深入讨论可参见 The Zen of SpacetimeDB。物理与逻辑独立性关系模型的一个核心目标是分离逻辑访问模式与物理数据表示。当你编写一条订阅查询时你表达的是需要什么数据而不是数据库应该如何检索它。这种分离使得 SpacetimeDB 可以出于性能原因改变数据的物理表示而无须你重写查询。最典型的例子是索引当你为某列添加索引时改变了该数据的物理组织方式——SpacetimeDB 会构建额外的数据结构来加速查找但你的订阅查询原样继续工作同一查询之前是全表扫描现在自动走索引你通过修改 schema 而非修改查询来获得性能提升。这种独立性还延伸到索引之外。SpacetimeDB 可以在不同版本间改变内部存储格式、内存布局与访问算法而你的查询保持稳定因为查询工作在逻辑层面行与列而非物理层面字节与指针。表分解Table Decomposition按访问模式组织数据设计关系 schema 时的一个常见疑虑是把数据合并进更少的大表还是分散到许多小表传统 SQL 数据库中join 需要冗长的查询语法并产生显著的执行开销这种摩擦促使开发者走向少而宽的反规范化 schema。SpacetimeDB 的约束完全不同你的 reducer 通过程序化 API而非 SQL 字符串与表交互。一次 join 操作退化为一次索引查找从一张表取出一行提取键值再用该键到另一张表中查找相关行。由于所有数据常驻内存这类查找往往在纳秒级完成。考虑一个游戏应用的 schema 设计合并式方案不推荐Player ├── id ├── name ├── position_x, position_y, velocity_x, velocity_y (updates: 60Hz) ├── health, max_health, mana, max_mana (updates: occasional) ├── total_kills, total_deaths, play_time (updates: rare) └── audio_volume, graphics_quality (updates: very rare)分解式方案推荐Player PlayerState PlayerStats PlayerSettings ├── id ←── ├── player_id ├── player_id ├── player_id └── name ├── position_x ├── total_kills ├── audio_volume ├── position_y ├── total_deaths └── graphics_quality ├── velocity_x └── play_time └── velocity_y PlayerResources ├── player_id ├── health ├── max_health ├── mana └── max_mana分解式方案带来多项优势降低带宽消耗订阅玩家位置的客户端在设置变更时不会收到更新。对于 1000 名并发玩家以 60Hz 更新位置的场景这种削减相当可观。缓存效率更新频率相近的数据驻留在连续内存中。更新玩家位置不需要加载或失效包含生涯统计的缓存行。语义清晰每张表职责单一。PlayerState服务性能关键的游戏循环PlayerStats服务排行榜查询PlayerSettings支撑设置界面。Schema 演化你可以向PlayerStats添加列而不影响PlayerState的结构或性能特征。指导原则按访问模式组织数据而不是按实体描述组织数据。把需要一起读取的数据放在同一张表中把在不同时间或不同频率读取的数据分开。Defining Tables三种语言的定义方式表在模块代码中定义包含名称、列和可选配置。三种服务端语言各有对应的声明语法。TypeScripttable函数import { table, t } from spacetimedb/server; const people table( { name: people, public: true }, { id: t.u32().primaryKey().autoInc(), name: t.string().index(btree), email: t.string().unique(), } );第一个参数定义表选项名称、可见性等第二个参数定义列。C#[SpacetimeDB.Table]特性[SpacetimeDB.Table(Name Person, Public true)] public partial struct Person { [SpacetimeDB.PrimaryKey] [SpacetimeDB.AutoInc] public uint Id; [SpacetimeDB.Index.BTree] public string Name; [SpacetimeDB.Unique] public string Email; }partial修饰符是必需的以便代码生成器扩展该类型。Rust#[spacetimedb::table]过程宏#[spacetimedb::table(name person, public)] pub struct Person { #[primary_key] #[auto_inc] id: u32, #[index(btree)] name: String, #[unique] email: String, }:::note Rust 可见性与 SpacetimeDB 可见性的区别 struct 上的pub修饰符遵循 Rust 常规可见性规则对 SpacetimeDB没有意义。它控制的是该结构体能否从 crate 内的其他 Rust 模块访问而不是表是否对客户端公开。请使用#[spacetimedb::table]中的public属性来控制客户端可见性。 :::表命名与访问器Accessor表名决定了你在代码中如何访问该表。理解这层关系对编写正确的 SpacetimeDB 模块至关重要。访问器名称如何派生TypeScript访问器名由 snake_case 转换为 camelCase// Table definition const player_scores table( { name: player_scores, public: true }, { /* columns */ } ); // Accessor uses camelCase ctx.db.playerScores.insert({ /* ... */ });Table NameAccessoruserctx.db.userplayer_scoresctx.db.playerScoresgame_sessionctx.db.gameSessionC#访问器名与Name特性值完全一致// Table definition [SpacetimeDB.Table(Name Player, Public true)] public partial struct Player { /* columns */ } // Accessor matches Name exactly ctx.Db.Player.Insert(new Player { /* ... */ });Name AttributeAccessorName Userctx.Db.UserName Playerctx.Db.PlayerName GameSessionctx.Db.GameSession:::warning 大小写敏感 访问器区分大小写必须与Name值完全匹配。Name user产生ctx.Db.user而不是ctx.Db.User。 :::Rust访问器名与name特性值完全一致// Table definition #[spacetimedb::table(name player, public)] pub struct Player { /* columns */ } // Accessor matches name exactly ctx.db.player().insert(Player { /* ... */ });name AttributeAccessorname userctx.db.user()name playerctx.db.player()name game_sessionctx.db.game_session()推荐命名约定LanguageConventionExample TableExample AccessorTypeScriptsnake_caseplayer_scorectx.db.playerScoreC#PascalCaseName PlayerScorectx.Db.PlayerScoreRustlower_snake_casename player_scorectx.db.player_score()这些约定与各语言的标准风格指南一致让代码在其生态中显得自然。表可见性私有与公开表默认是私有的也可以设为公开私有表仅 reducers 与数据库所有者可见客户端无法访问公开表通过 subscriptions 向客户端开放读取权限写入仍然只通过 reducers 进行。三种语言的定义方式const publicTable table({ name: user, public: true }, { /* ... */ }); const privateTable table({ name: secret, public: false }, { /* ... */ });[SpacetimeDB.Table(Name User, Public true)] public partial struct User { /* ... */ } [SpacetimeDB.Table(Name Secret, Public false)] public partial struct Secret { /* ... */ }#[spacetimedb::table(name user, public)] pub struct User { /* ... */ } #[spacetimedb::table(name secret)] pub struct Secret { /* ... */ }如果需要更细粒度的访问控制可以使用 view functions 向客户端暴露经过计算的数据子集——视图可以在暴露前过滤行、选择特定列或连接多张表的数据。可见性与访问模式的完整细节参见 Access Permissions私有表适合内部配置、密码哈希/API 密钥等敏感数据与中间计算结果公开表适合客户端需要展示或交互的数据游戏状态、用户资料等。reducers 持有对全部表公开与私有的完整读写权限增删改查客户端则只能读取公开表数据并通过调用 reducers 间接修改。同一类型的多张表你可以让多张表共享同一个行类型——对单个 struct 应用多个 table 特性即可。每张表独立存储自己的行集合但共享同一 schema。TypeScript定义共享同一列 schema 的多个表变量import { table, t } from spacetimedb/server; // Define the shared column schema const playerColumns { identity: t.Identity.primaryKey(), playerId: t.i32().unique().autoInc(), name: t.string(), }; // Create two tables with the same schema const Player table({ name: Player, public: true }, playerColumns); const LoggedOutPlayer table({ name: LoggedOutPlayer }, playerColumns);C#对同一 struct 应用多个[Table]特性[SpacetimeDB.Table(Name Player, Public true)] [SpacetimeDB.Table(Name LoggedOutPlayer)] public partial struct Player { [PrimaryKey] public Identity Identity; [Unique, AutoInc] public int PlayerId; public string Name; }每张表都有自己的访问器// Insert into different tables ctx.Db.Player.Insert(new Player { /* ... */ }); ctx.Db.LoggedOutPlayer.Insert(new Player { /* ... */ }); // Move a row between tables var player ctx.Db.LoggedOutPlayer.Identity.Find(ctx.Sender); if (player ! null) { ctx.Db.Player.Insert(player.Value); ctx.Db.LoggedOutPlayer.Identity.Delete(player.Value.Identity); }Rust对同一 struct 应用多个#[spacetimedb::table]特性#[spacetimedb::table(name player, public)] #[spacetimedb::table(name logged_out_player)] pub struct Player { #[primary_key] identity: Identity, #[unique] #[auto_inc] player_id: i32, name: String, }每张表都有自己的访问器// Insert into different tables ctx.db.player().insert(Player { /* ... */ }); ctx.db.logged_out_player().insert(Player { /* ... */ }); // Move a row between tables if let Some(player) ctx.db.logged_out_player().identity().find(ctx.sender) { ctx.db.player().insert(player.clone()); ctx.db.logged_out_player().identity().delete(player.identity); }这一模式适用于状态管理区分活跃用户与非活跃用户、在线玩家与离线玩家归档将旧记录迁移到归档表同时保持相同 schema暂存Staging将待处理记录暂存在一张表中之后移入主表。:::note 共享约束[PrimaryKey]、[Unique]、[AutoInc]、[Index]等列特性会应用到该类型定义的所有表。每张表都会拥有结构相同但相互独立的主键、唯一约束与索引。 :::约束Constraints表支持若干约束以强制数据完整性主键Primary Keys唯一标识每一行并定义更新与删除的行为唯一约束Unique Constraints确保没有两行在某列上共享相同值。完整细节参见 Constraints核心要点包括每张表最多一个主键主键定义了行的身份修改主键值等同于删除旧行并插入新行订阅者会先收到 delete 事件再收到 insert 事件主键天然唯一SpacetimeDB 通过自动创建的唯一索引实现目前不支持多列组合主键需要按多列查找时可使用自增主键 多列 btree 索引的组合表可以没有主键此时整行充当主键行由其完整内容标识、重复行不可能出现插入完全相同的行无效果、更新需要匹配所有字段——SpacetimeDB 始终维护集合语义区别只在于唯一性的定义者是主键列还是整行唯一列可以有多个且同样会创建索引以支持高效查找。主键定位行身份、唯一列保障数据完整性更新行为上前者是 delete insert后者是原地更新。自增列Auto-Increment自增列自动为新行生成唯一的整数值。SpacetimeDB 使用**序列sequences**实现自增——一种参照 PostgreSQL 序列设计的机制提供崩溃安全的值生成与可配置参数。完整细节参见 Auto-Increment。关键行为自增列必须是整数类型u8到u64、i8到i64等触发值插入时该列传 0 则触发自增传非零值则直接使用该值可用于迁移带已知 ID 的存量数据序列参数包括start起始值、min_value最小值、max_value最大值、increment步长可为负回绕行为序列到达最大值后回绕到最小值继续。如min_value 1, max_value 10, increment 1的序列产生 1,2,...,10,1,2,...崩溃恢复序列按4096 个一批分配值耗尽时先持久化分配边界再发值。数据库重启后从下一个分配边界继续——可能跳过已分配未使用的值但保证绝不重复赋值。例如插入 Alice/Bob/Carol 得到 id 1、2、3重启后插入 Dave 会得到 4097不保证连续崩溃恢复跳号、并发事务SpacetimeDB 当前串行执行事务但保留未来并发执行的权利都可能导致缺口。同一 reducer 内连续插入也不保证产生连续值若应用需要严格连续编号如发票号请在一个独立计数表中显式维护计数器计数器更新与行插入发生在同一事务内即可保证顺序自增可与主键、唯一约束组合但不能与默认值组合两者都会自动填充该列。调度表Schedule Tables表可以通过包含一个调度列scheduling column在特定时间触发 reducers。这使得你可以安排未来的动作例如发送提醒、内容过期或周期性维护。详见 Schedule Tables。更进一步表设计最佳实践围绕本文提到的能力表设计性能指南 提供了最佳实践汇总。结合前文内容核心设计要点可归纳为按访问频率分解表高频更新位置与低频更新设置、统计分离降低订阅带宽与缓存失效代价索引服务于查询为查询条件列建索引利用物理/逻辑独立性在不改查询的前提下提升性能可见性最小化客户端不需要的数据一律私有公开表只暴露必要字段必要时用视图做细粒度裁剪主键即身份优先使用自增主键或Identity主键全表迭代场景可省略主键换取性能自增用于唯一标识、计数表用于连续编号明确区分唯一与连续两种需求。相关阅读Next StepsColumn Types —— 支持的列类型与性能考量Constraints —— 主键与唯一约束Auto-Increment —— 基于序列的自动 ID 生成Default Values —— 借助列默认值进行 schema 演化Indexes —— 用单列与多列索引加速查询Access Permissions —— 公开表与私有表Schedule Tables —— 基于时间的 reducer 执行Performance —— 表设计最佳实践【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表