拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Chatto事件溯源架构揭秘:为什么用NATS JetStream替代关系型数据库

Chatto事件溯源架构揭秘:为什么用NATS JetStream替代关系型数据库

Chatto事件溯源架构揭秘:为什么用NATS JetStream替代关系型数据库

【免费下载链接】chattoA fully-featured team and group chat application that you can easily selfhost.项目地址: https://gitcode.com/gh_mirrors/chatt/chatto

Chatto 是一款功能完整的团队与群组聊天应用,主打免费、轻松自托管。它最"反直觉"的设计是:整套系统不使用任何关系型数据库,而是把开源消息系统NATS JetStream当作唯一的数据存储,并用**事件溯源(Event Sourcing)**来管理全部业务状态。这篇文章面向新手,用尽量少的代码,讲清楚这个架构的来龙去脉和真实取舍。

一眼看懂:一个"没有数据库"的聊天应用

Chatto 支持房间、线程、@提及、表情回应、文件上传、语音通话、机器人、管理员面板等完整功能。下面就是它的聊天界面:

通常这类应用需要一整套基础设施:

  • Web 服务器
  • 关系型数据库(PostgreSQL / MySQL)
  • 消息队列(做实时推送)
  • 对象存储 / 缓存

每一样都意味着部署、备份、连接池、版本兼容问题。而 Chatto 的做法是:一个可执行文件,一个端口,一个数据目录,全部搞定。

传统做法的痛点:数据库 + 消息队列"二件套"

传统聊天应用里,数据流是这样的:

  1. 写入 → 存进关系型数据库(一条INSERT语句)
  2. 推送 → 再发一条消息到 Pub/Sub 系统,前端通过 WebSocket 收到
  3. 备份 → 数据库、消息队列、缓存各备各的

这里有个隐藏成本:存储层和实时推送层是两套系统,服务同一份数据。你需要保证两者最终一致,需要 CDC、轮询或同步层,还要为每套系统分别做运维和备份。

而聊天领域的本质恰好是事件驱动的:消息是事件、在线状态是事件、"正在输入"是事件。存储和推送处理的本来就是同一份数据——那为什么要拆成两个系统?

核心决策:NATS JetStream 作为唯一数据底座

这个决策记录在 docs/adr/ADR-001-nats-jetstream-as-primary-data-store.md 中,关键结论是:

使用 NATS JetStream 作为唯一持久化存储。没有关系型数据库、没有 ORM、没有 SQL 迁移。

具体来说,JetStream 提供了几种"容器",各司其职:

类型名字存什么新手理解
Stream(事件流)EVT所有领域事实:用户、房间、消息、成员关系一本永不擦除的"流水账"
KV 桶RUNTIME_STATE最新值状态:凭据、在线设置、通知边界一张只保留最新值的"便签纸"
KV 桶MEMORY_CACHE易失的租约、计数器、健康心跳内存里的临时草稿
对象存储SERVER_ASSETS上传的图片、文件等二进制仓库货架
NATS Corelive.sync.>不持久化的实时信号(打字、在线状态)广播喇叭

完整清单见 docs/architecture/nats-resources.md。

事件溯源是什么?用"餐厅账本"打比方 📒

事件溯源的核心思想一句话:不记录"现在是什么样",只记录"发生了什么"。

就像餐厅不记录"当前库存 50 个鸡蛋",而是记录"购入 100 个""用了 30 个""退回 20 个"。任何时候想知道库存,把账本从头重放一遍就能算出来。

Chatto 的EVT流就是这样一本账本:

  • 用户加入房间 → 追加一条user_joined事件
  • 发送消息 → 追加一条message_posted事件
  • 编辑消息 →不修改原事件,再追加一条message_edited事件

所谓"当前状态"(比如某个房间现在有哪些成员),由**投影(Projection)**从事件流实时推导出来——大多数投影只是进程内存里的数据结构,启动时重放事件流即可重建。这一演进过程记录在 docs/adr/ADR-033-event-sourced-state-with-projections.md 中(更早的"KV 存现状 + 流存历史"模式见已被取代的 ADR-006)。

这套设计带来四个实打实的好处:

  1. 审计日志是天然的:审计追踪不是附加的旁路,数据本身就是日志
  2. 改数据结构 = 重建投影:加字段、改推导逻辑,只要"丢掉投影、重放事件流",不需要写数据迁移脚本
  3. 写入原语统一:所有写操作都走"带乐观并发控制(OCC)追加事件"这一条路,代码里只有一种变更模式
  4. 内存占用可控:主题(subject)数量只随聚合体(房间、用户)增长,而不随消息数量无界增长

单流设计:为什么所有事件只写进EVT

一个自然的疑问:消息、用户、房间……难道不该分多条流吗?

docs/adr/ADR-034-single-event-stream.md 选择了单条主事件流,因为:

  • 备份时只有一个目标、一个位置要跟踪
  • 运维工具面对的资源更少
  • 顺序保证不依赖"每聚合一条流":JetStream 对每个主题内部都有独立的单调序列号,房间 X 的事件天然线性有序

事件流的地址格式为evt.{聚合类型}.{聚合ID}.{事件类型},例如evt.room.{房间ID}.message_posted。投影只需订阅自己关心的主题前缀,服务端就会过滤——关心"谁加入了房间"的投影根本不会收到海量消息事件。

并发安全:乐观并发控制(OCC)

多人同时操作同一个房间怎么办?Chatto 的答案是强制 OCC:

  • 每次追加事件都携带Nats-Expected-Last-Subject-Sequence头,声明"我以为该聚合的最新序号是 N"
  • 如果实际序号已变(说明有人抢先写入),框架整体重跑决策,基于新状态重新判断
  • 框架不提供"不带 OCC 的写入"原语——想跳过并发检查都做不到

代价是热点聚合上可能出现重试循环,但换来的是整个代码库再无"我是不是和别人撞车了?"这类问题。

自托管者的福音:备份与恢复只需一条命令

事件溯源让备份变得异常简单——整个数据边界就是 NATS 里的几类资源:

  • chatto backup把EVT、RUNTIME_STATE、通知历史、NATS 资产、投影快照打包成一个压缩归档,可选 age 口令加密
  • chatto restore在停服的部署上恢复该归档,恢复后从EVT重放即可重建全部投影

细节见 docs/fdr/FDR-040-backup-and-restore.md,命令实现在 cli/cmd/backup.go。对比传统方案"数据库 dump + 对象存储同步 + 队列状态协调"的多步骤流程,这是显著简化。

部署形态:嵌入式 NATS,一条命令启动

docs/adr/ADR-002-single-binary-with-embedded-nats.md 决定了:NATS 服务器以库的形式内嵌在 Chatto 的 Go 二进制里。

  • chatto run一条命令启动 NATS、HTTP API/实时服务器和内嵌的前端
  • 单节点模式下应用和数据存储之间零网络调用,没有连接池、重连、网络分区问题
  • 升级是原子的:换一个二进制、重启一次进程
  • 高级场景仍可外接 NATS 集群实现水平扩展——嵌入式只是起点,不是上限

坦诚的代价:这个架构放弃了什么?

没有免费的午餐,官方 ADR 也明确列出了代价:

  • 没有任意 SQL 查询:JetStream 不是查询引擎,没有 JOIN。相关数据靠投影和 API 组装层拼装
  • 全文搜索要另做:需要专门的投影或可插拔搜索系统(见 ADR-055),EVT本身不支持LIKE '%xxx%'
  • 重启成本随流长度增长:没有可用快照时,重启要花时间重放事件流(可选的加密快照只是加速器,正确性基线仍是完整重放)
  • 运维知识转移:运维者要懂 NATS 流、消费者、KV 语义,而不是 SQL 和数据库调优
  • 跨进程一致性是最终一致:两个进程对同一份状态的读取可能有亚毫秒级的瞬间不一致——对聊天场景可接受

延伸阅读:架构是怎么"长"出来的 📚

Chatto 的文档体系非常值得学习——每个关键决策都有 ADR(架构决策记录),每个功能行为都有 FDR(功能决策记录):

想深入了解去哪里看
为什么选 NATS JetStreamdocs/adr/ADR-001-nats-jetstream-as-primary-data-store.md
单二进制 + 嵌入式 NATSdocs/adr/ADR-002-single-binary-with-embedded-nats.md
事件溯源 + 投影模式docs/adr/ADR-033-event-sourced-state-with-projections.md
单事件流EVT设计docs/adr/ADR-034-single-event-stream.md
运行时状态与事件流的边界docs/adr/ADR-036-runtime-state-kv-boundary.md
NATS 资源完整清单docs/architecture/nats-resources.md
事件溯源框架(独立模块)pkg/events/README.md
架构总览索引docs/ARCHITECTURE.md

总结

Chatto 用一个大胆但自洽的架构回答了"自托管聊天应用"的复杂度问题:

  1. 存储与推送合一——JetStream 既是数据库也是消息总线,消灭了同步层
  2. 事件溯源——EVT流是唯一事实来源,状态是派生物,审计、迁移、恢复全部简化
  3. 单二进制部署——嵌入式 NATS 让"一条命令跑起来"成为现实
  4. 备份即打包——一个归档搞定全部持久化数据

对普通用户而言,你只需要chatto run一个命令;对进阶部署者,外接 NATS 集群就是水平扩展路径。这就是事件溯源 + NATS JetStream 组合的完整故事:用领域模型的自然形态(事件)来设计存储,而不是把领域硬塞进一张张关系表。

【免费下载链接】chattoA fully-featured team and group chat application that you can easily selfhost.项目地址: https://gitcode.com/gh_mirrors/chatt/chatto

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

返回列表