这篇文章将深入剖析SpacetimeDB的技术架构,看看这个"颠覆传统"的数据库如何重新定义实时应用的后端开发范式。前言:一个困扰开发者多年的问题如果你开发过实时游戏、聊天应用或协作工具,你一定经历过这样的架构:客户端→API服务器→数据库↓
这篇文章将深入剖析 SpacetimeDB 的技术架构,看看这个"颠覆传统"的数据库如何重新定义实时应用的后端开发范式。

如果你开发过实时游戏、聊天应用或协作工具,你一定经历过这样的架构:
客户端 → API服务器 → 数据库
↓
WebSocket服务
↓
消息队列
↓
缓存层...
每一层都需要部署、监控、扩展。一个简单的游戏功能,可能需要微服务、Kubernetes、Docker、负载均衡器...基础设施的复杂度远超业务逻辑本身。
如果我说,这些都可以简化为一个数据库,你信吗?
SpacetimeDB 就是这样做的——把应用服务器"塞进"数据库里。
SpacetimeDB 是一个专为实时应用设计的数据库系统,它将传统的关系型数据库与应用服务器的功能合二为一。
听起来很抽象?看这个对比:
| 传统架构 | SpacetimeDB 架构 |
|---|---|
| 数据库 + API服务器 + WebSocket | 仅一个数据库 |
| 客户端连接到服务器,服务器查询数据库 | 客户端直接连接数据库 |
| 实时推送需要手动实现 WebSocket | 内置订阅系统,自动推送更新 |
| 后端代码部署到服务器 | 后端代码部署到数据库(WASM模块) |
真实案例:MMORPG 游戏 BitCraft Online 的整个后端——聊天消息、物品系统、资源管理、地形数据、玩家位置——全部运行在 SpacetimeDB 上。
SpacetimeDB 的架构设计堪称精妙:

这是 SpacetimeDB 最激进的设计决策之一:所有应用状态都保存在内存中。
// 数据通过 WAL (Write-Ahead Log) 保证持久化
pub trait Durability: Send + Sync {
type TxData;
fn append_tx(&self, tx: Self::TxData); // 追加事务日志
fn durable_tx_offset(&self) -> DurableOffset; // 获取持久化位置
}
为什么敢这么做?
这就像 Redis 的持久化模式,但内置了完整的关系型查询引擎。
SpacetimeDB 的"模块"本质上是一个编译为 WebAssembly (WASM) 的程序,运行在数据库内部:
// 定义一张表 - 就像定义一个结构体
#[spacetimedb::table(public)]
pub struct Player {
#[primary_key]
id: u64,
name: String,
position: (f32, f32, f32),
}
// 定义一个 Reducer - 就像定义一个 API 端点
#[spacetimedb::reducer]
pub fn move_player(ctx: &ReducerContext, player_id: u64, new_pos: (f32, f32, f32)) {
if let Some(mut player) = ctx.db.player().id().find(player_id) {
player.position = new_pos;
ctx.db.player().id().update(player);
}
}
// 生命周期钩子
#[spacetimedb::reducer(init)]
pub fn init(ctx: &ReducerContext) {
// 模块首次部署时调用
}
#[spacetimedb::reducer(client_connected)]
pub fn on_connect(ctx: &ReducerContext) {
// 客户端连接时调用
}
每个 Reducer 都在一个独立的事务中执行——原子性、一致性自动保证。
这是 SpacetimeDB 最强大的功能之一。传统实现实时推送需要:
SpacetimeDB 通过增量视图维护自动完成这一切:
/// 订阅计划:使用增量计算避免全量查询重算
/// 对于连接查询:dv = R'ds(+) U dr(+)S' U dr(-)ds(-) U dr(-)ds(+)
/// 这避免了每次更新都计算 R' x S'
pub struct SubscriptionPlan {
return_id: TableId,
table_ids: Vec<TableId>,
fragments: Fragments, // 插入/删除计划片段
}
客户端订阅示例(TypeScript):
const conn = DbConnection.builder()
.withUri('ws://localhost:3000')
.withDatabaseName('game_module')
.onConnect((ctx) => {
// 订阅查询 - 自动推送更新
ctx.subscriptionBuilder()
.onApplied(() => console.log('订阅就绪!'))
.subscribe([`SELECT * FROM player WHERE zone = ${myZone}`]);
})
.build();
// 监听变更事件
conn.db.player.onInsert((ctx, player) => {
console.log(`新玩家加入: ${player.name}`);
});
SpacetimeDB 的代码组织堪称 Rust 项目的典范:
| 分类 | Crate | 职责 |
|---|---|---|
| 核心运行时 | core, standalone, cli |
数据库服务器和命令行工具 |
| 数据层 | datastore, table, commitlog |
内存表、WAL 持久化 |
| 查询引擎 | vm, execution, expr, sql-parser |
SQL 解析、查询规划、执行 |
| 类型系统 | sats, schema, primitives |
Spacetime 代数类型系统 |
| 模块系统 | bindings, bindings-macro, codegen |
WASM 模块运行时和宏 |
| 客户端 API | client-api, client-api-messages |
HTTP/WebSocket API |
| 实时推送 | subscription |
订阅和增量视图维护 |
文件位置参考:
crates/core/src/lib.rscrates/standalone/src/lib.rscrates/subscription/src/lib.rscrates/datastore/src/traits.rsSpacetimeDB 没有使用现成的序列化格式,而是设计了 SATS (Spacetime Algebraic Type System):
// 支持的基础类型
pub use algebraic_type::AlgebraicType;
pub use algebraic_value::{i256, u256, AlgebraicValue, F32, F64};
// 复合类型
pub use product_type::ProductType; // 结构体
pub use sum_type::SumType; // 枚举
pub use typespace::Typespace; // 类型空间
支持的数据类型:
bool, i8-i256, u8-u256, f32, f64String, Bytes, Identity, Timestamp, Uuid这种设计确保了跨语言的一致性——Rust、C#、TypeScript 都能无缝对接。
SpacetimeDB 提供了完整的 SDK 生态:
| SDK | 服务端模块支持 | 客户端库 | 主要用途 |
|---|---|---|---|
| Rust | ✅ | ✅ | 原生应用、高性能场景 |
| C#/.NET | ✅ | ✅ | Unity 游戏、.NET 应用 |
| TypeScript | ❌ | ✅ | Web、React、Next.js |
| Unreal | ❌ | ✅ | Unreal Engine 游戏 |
Rust 服务端模块示例:
use spacetimedb::{ReducerContext, Table};
#[spacetimedb::table(public)]
pub struct Message {
#[primary_key]
#[auto_inc]
id: u64,
sender: Identity,
content: String,
timestamp: Timestamp,
}
#[spacetimedb::reducer]
pub fn send_message(ctx: &ReducerContext, content: String) {
ctx.db.message().insert(Message {
id: 0, // auto_inc
sender: ctx.sender,
content,
timestamp: ctx.timestamp,
});
}
C# Unity 客户端示例:
public class GameManager : MonoBehaviour
{
private DbConnection _conn;
async void Start()
{
_conn = await DbConnection.Builder()
.WithUri("ws://localhost:3000")
.WithDatabaseName("chat_module")
.Build();
// 订阅消息表
_conn.Db.Message.OnInsert += (ctx, msg) => {
Debug.Log($"新消息: {msg.Content}");
};
await _conn.Subscribe("SELECT * FROM message");
}
public void SendMessage(string content)
{
_conn.Reducers.SendMessage(content);
}
}
SpacetimeDB 实现了完整的事务隔离支持:
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum IsolationLevel {
ReadUncommitted, // 读未提交
ReadCommitted, // 读已提交
RepeatableRead, // 可重复读
Snapshot, // 快照隔离 - 防止脏读、不可重复读、幻读
Serializable, // 可串行化 - 最高隔离级别
}
每个 Reducer 自动在独立事务中执行,失败自动回滚。
| 特性 | 传统架构(数据库 + 服务器) | SpacetimeDB |
|---|---|---|
| 部署复杂度 | 多个服务、容器编排 | 单一二进制文件 |
| 延迟 | 客户端→服务器→数据库→服务器→客户端 | 客户端↔数据库 |
| 实时推送 | 手动实现 WebSocket | 内置订阅系统 |
| 基础设施 | Kubernetes、Docker、负载均衡... | 无需 DevOps |
| 开发语言 | 后端语言 + SQL + 前端语言 | 单一语言(Rust/C#) |
| 事务支持 | 手动管理 | 自动事务 |
| 扩展性 | 水平扩展服务器 | 垂直扩展 + 分片 |
SpacetimeDB 采用 BSL 1.1 许可证,4 年后自动转为 AGPL v3.0 + 链接例外。
这意味着:
SpacetimeDB 代表了一种范式转移:
传统思维:数据库存储数据,服务器处理逻辑
SpacetimeDB:数据库既存储数据,又处理逻辑
这种转变带来的好处:
当然,这不是银弹。对于需要复杂业务逻辑、大量后台任务的应用,传统架构可能更合适。
但对于实时游戏、聊天、协作工具这些场景,SpacetimeDB 提供了一个令人兴奋的新选择。
# 安装 CLI
cargo install spacetimedb-cli
# 启动本地服务器
spacetime start
# 创建新项目
spacetimedb new my-game --lang rust
# 发布模块
spacetime publish my-game
项目地址:https://github.com/clockworklabs/SpacetimeDB
文档:https://spacetimedb.com/docs
本文基于 SpacetimeDB 源码分析撰写,感谢 Clockwork Labs 团队的开源贡献。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!