关于 spacetimedb-concepts
spacetimedb-concepts 是使用 SpacetimeDB 构建和调试应用的参考技能。SpacetimeDB 是可在 WebAssembly 模块中运行应用逻辑的关系型数据库;该技能介绍表、事务 Reducer、订阅、身份验证及客户端集成。
主要功能
- 01说明表与 Reducer 的设计
- 02介绍客户端实时订阅
- 03讲解模块生命周期与发布
- 04提供身份验证和授权指引
- 05包含实现与调试检查清单
适用场景
- 01规划数据表和修改数据的 Reducer
- 02将客户端视图连接到订阅数据
- 03排查模块未发布或未被调用的问题
- 04构建实时游戏或协作应用
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ clockworklabs/ spacetimedb --skill spacetimedb-concepts安装后在 Claude Code 中输入/spacetimedb-concepts确认已加载。
详细介绍
SpacetimeDB 核心概念
SpacetimeDB 是一个同时也是服务器的关系型数据库。它允许你通过 WebAssembly 模块将应用逻辑直接上传到数据库中,从而彻底省去传统的 Web/游戏服务器层。
重要规则(请先阅读)
以下五条规则有助于避免最常见的 SpacetimeDB 错误:
- Reducer 是事务性的 — 它们不会向调用方返回数据。使用订阅读取数据。
- Reducer 必须是确定性的 — 不得使用文件系统、网络、计时器或随机数。所有状态都必须来自表。
- 通过表/订阅读取数据 — 不要依赖 Reducer 的返回值。客户端通过已订阅的查询获取数据。
- 自动递增 ID 并非连续 — 出现间隙很正常,不要用它排序。请使用时间戳或显式的序列列。
ctx.sender()是经过身份验证的主体 — 绝不要信任作为参数传入的身份。授权时务必使用ctx.sender()。
功能实现检查清单
实现一个涉及后端和客户端的功能时:
- 后端: 定义用于存储数据的表
- 后端: 定义用于修改数据的 Reducer
- 客户端: 订阅相关表
- 客户端: 在 UI 中调用 Reducer — 不要跳过这一步
- 客户端: 从表中渲染数据
常见错误: 构建了后端表和 Reducer,却忘记在客户端接入并调用它们。
调试检查清单
遇到问题时:
- SpacetimeDB 服务是否正在运行?(
spacetime start) - 模块是否已发布?(
spacetime publish) - 是否已生成客户端绑定?(
spacetime generate) - 检查服务日志是否有错误(
spacetime logs <db-name>) - 客户端是否实际调用了 Reducer?
CLI 命令
spacetime start
spacetime publish <db-name> --module-path <module-path>
spacetime publish <db-name> --clear-database -y --module-path <module-path>
spacetime generate --lang <lang> --out-dir <out> --module-path <module-path>
spacetime logs <db-name>
SpacetimeDB 是什么
SpacetimeDB 将数据库和应用服务器合并为一个可部署单元。客户端直接连接到数据库,并在其中执行应用逻辑。该系统针对需要极致速度和最低延迟的实时应用进行了优化。
主要特征:
- 内存中执行:应用状态由内存提供,以实现极低延迟的访问
- 持久化存储:数据会自动持久化到预写日志(WAL),以保证持久性
- 实时同步:更改会自动推送给已订阅的客户端
- 单一部署:无需管理独立的服务器、容器或基础设施
五大禅意原则
- 一切皆为表:整个应用状态都存储在表中。无需单独的缓存层、Redis 或需要同步的内存状态。
- 一切皆持久:SpacetimeDB 默认持久化状态(例如通过 WAL 支持的持久性)。
- 一切皆实时:客户端是服务器状态的副本。订阅数据后,数据就会自动流动。
- 一切皆事务性:每个 Reducer 都会原子执行。所有更改要么全部成功,要么全部回滚。
- 一切皆可编程:模块是真正的代码(Rust、C#、TypeScript),运行在数据库内部。
表
表存储 SpacetimeDB 中的所有数据。它们采用关系模型,并支持用于订阅的 SQL 查询。
定义表
表通过特定语言的属性定义。在 2.0 中,API 名称请使用 accessor(而不是 name):
Rust:
#[spacetimedb::table(accessor = player, public)]
pub struct Player {
#[primary_key]
#[auto_inc]
id: u32,
#[index(btree)]
name: String,
#[unique]
email: String,
}
C#:
[SpacetimeDB.Table(Accessor = "Player", Public = true)]
public partial struct Player
{
[SpacetimeDB.PrimaryKey]
[SpacetimeDB.AutoInc]
public uint Id;
[SpacetimeDB.Index.BTree]
public string Name;
[SpacetimeDB.Unique]
public string Email;
}
TypeScript:
const players = table(
{ name: 'players', public: true },
{
id: t.u32().primaryKey().autoInc(),
name: t.string().index('btree'),
email: t.string().unique(),
}
);
表可见性
- 私有表(默认):只能由 Reducer 和数据库所有者访问
- 公共表:可通过订阅向客户端开放读取权限。写入仍需通过 Reducer。
表设计原则
根据访问模式而不是实体来组织数据:
分解式设计(推荐):
Player PlayerState PlayerStats
id <-- player_id player_id
name position_x total_kills
position_y total_deaths
velocity_x play_time
优点:减少带宽占用、提高缓存效率、便于演进架构、语义清晰。
Reducer
Reducer 是用于修改数据库状态的事务性函数。它们是客户端调用的主要修改途径;过程也可以通过运行显式事务来修改表。
主要属性
- 事务性:在隔离的数据库事务中运行
- 原子性:要么所有更改都成功,要么全部回滚
- 隔离性:无法与外部世界交互(不能访问网络或文件系统)
- 可调用:客户端通过远程过程调用来调用 Reducer
Reducer 的关键规则
- 不得使用全局状态:依赖静态变量属于未定义行为
- 不得产生副作用:Reducer 无法发起网络请求或访问文件
- 将状态存储在表中:所有持久化状态都必须存储在表中
- 不返回数据:Reducer 不向调用方返回数据——请使用订阅
- 必须具有确定性:不得使用随机数、计时器或外部 I/O
定义 Reducer
Rust:
#[spacetimedb::reducer]
pub fn create_user(ctx: &ReducerContext, name: String, email: String) -> Result<(), String> {
if name.is_empty() {
return Err("Name cannot be empty".to_string());
}
ctx.db.user().insert(User { id: 0, name, email });
Ok(())
}
C#:
[SpacetimeDB.Reducer]
public static void CreateUser(ReducerContext ctx, string name, string email)
{
if (string.IsNullOrEmpty(name))
throw new ArgumentException("Name cannot be empty");
ctx.Db.User.Insert(new User { Id = 0, Name = name, Email = email });
}
ReducerContext
每个 Reducer 都会接收一个 ReducerContext,其中提供:
- 数据库:
ctx.db(Rust 字段、TS 属性)/ctx.Db(C# 属性) - 发送方:
ctx.sender()(Rust 方法)/ctx.Sender(C# 属性)/ctx.sender(TS 属性) - 连接 ID:
ctx.connection_id()(Rust 方法)/ctx.ConnectionId(C# 属性)/ctx.connectionId(TS 属性) - 时间戳:
ctx.timestamp(Rust 字段、TS 属性)/ctx.Timestamp(C# 属性)
事件表(2.0)
事件表是向客户端广播特定于 Reducer 的数据的首选方式。
#[table(accessor = damage_event, public, event)]
pub struct DamageEvent {
pub target: Identity,
pub amount: u32,
}
#[reducer]
fn deal_damage(ctx: &ReducerContext, target: Identity, amount: u32) {
ctx.db.damage_event().insert(DamageEvent { target, amount });
}
客户端订阅事件表并使用 on_insert 回调。必须显式订阅事件表,且它们不会包含在 subscribe_to_all_tables() 中。
订阅
订阅会将数据库行实时同步到客户端。
订阅的工作方式
- 订阅:注册用于描述所需数据的 SQL 查询
- 接收初始数据:立即发送所有匹配的行
- 接收更新:订阅的行发生变化时实时接收更新
- 响应更改:使用回调(
onInsert、onDelete、onUpdate)
订阅最佳实践
- 按生命周期分组订阅:将始终需要的数据与临时订阅分开
- 先订阅,再取消订阅:更新订阅时,先订阅新数据
- 避免查询重叠:返回重叠数据的不同查询会导致重复处理
- 使用索引:对已索引列的查询效率高;全表扫描开销很大
模块
模块是包含应用逻辑的 WebAssembly 程序包,在数据库内部运行。
模块组件
- 表:定义数据架构
- Reducer:定义可调用的状态修改函数
- 视图:定义只读的计算查询
- 事件表:向客户端广播特定于 Reducer 的数据(2.0)
- 过程:(Beta)可产生副作用的函数(HTTP 请求)
模块语言
服务器端模块可使用以下语言编写:Rust、C#、TypeScript(beta)
模块生命周期
- 编写:使用所选语言定义表和 Reducer
- 编译:使用 SpacetimeDB CLI 构建为 WebAssembly
- 发布:使用
spacetime publish上传到 SpacetimeDB 主机 - 热切换:重新发布以更新代码,无需断开客户端连接
身份
身份是 SpacetimeDB 基于 OpenID Connect(OIDC)的身份验证系统。
- Identity:用户长期有效的全局唯一标识符。
- ConnectionId:标识特定的客户端连接。
#[spacetimedb::reducer]
pub fn do_something(ctx: &ReducerContext) {
let caller_identity = ctx.sender(); // Who is calling?
// NEVER trust identity passed as a reducer argument
}
身份验证提供商
SpacetimeDB 支持许多 OIDC 提供商,包括 SpacetimeAuth(内置)、Auth0、Clerk、Keycloak、Google 和 GitHub。
何时使用 SpacetimeDB
理想用例
- 实时游戏:大型多人在线游戏、多人游戏、回合制游戏
- 协作应用:文档编辑、白板、设计工具
- 聊天和消息传递:支持在线状态的实时通信
- 实时仪表盘:流式分析和监控
关键决策因素
在以下情况下选择 SpacetimeDB:
- 读写延迟低于 10 ms
- 自动实时同步
- 所有操作都有事务保障
- 架构更简单(无需单独的缓存、队列或服务)
不太适用的场景
- 批量分析:针对 OLTP 优化,而非 OLAP
- 大型 Blob 存储:更适合结构化关系数据
- 无状态 API:传统 REST API 无需实时同步
常见模式
在 reducer 中检查身份验证:
#[spacetimedb::reducer]
fn admin_action(ctx: &ReducerContext) -> Result<(), String> {
let admin = ctx.db.admin().identity().find(&ctx.sender())
.ok_or("Not an admin")?;
Ok(())
}
计划任务 reducer:
#[spacetimedb::table(accessor = reminder, scheduled(send_reminder))]
pub struct Reminder {
#[primary_key]
#[auto_inc]
id: u64,
scheduled_at: ScheduleAt,
message: String,
}
#[spacetimedb::reducer]
fn send_reminder(ctx: &ReducerContext, reminder: Reminder) {
log::info!("Reminder: {}", reminder.message);
}
编辑行为
修改 SpacetimeDB 代码时:
- 只进行必要的最小改动
- 不要修改无关文件、配置或依赖项
- 不要臆造新的 SpacetimeDB API——仅使用文档或此代码库中已有的 API