第二篇:实时团队邀请的架构与数据流
本篇目标:从业务正确性出发设计实时链路,理解为什么"消息是提醒,REST 才是账本"。
1. 原始问题不是"缺一个 WebSocket"
Alice 提交 Bob 的邮箱,API 写入成员关系并向 Alice 返回成功,但 Bob 没有发请求,因此页面仍是旧列表。需求实际包含三件事:数据库可靠记录成员关系;Bob 在线时尽快知道;Bob 的页面最终与数据库一致。三件事不能全交给一条实时消息。
2. 三层职责
- PostgreSQL 保存业务事实,当前邀请使用单条 INSERT 的原子性,并由外键和唯一约束兜底;
- Socket.IO 发送 team.membership.created 变化提醒,优化感知速度;
- REST 读取权威快照,Bob 收到事件后重新 GET /api/teams。
3. 完整时序
必须先提交数据库再发事件。若顺序相反,Bob 回源可能查不到成员;保存失败时还会收到错误的成功通知。
这里的"先提交"是指 repository.save 对单条成员记录的写入成功;当前 addTeamMember 没有把前置查询、INSERT 和通知包在一个显式业务事务中。数据库唯一约束负责处理检查与写入之间的竞争窗口。
4. 先画出数据模型,避免把连接当成员关系
系统里至少有四种不同对象:
- User:注册账号;
- Team:团队;
- TeamMember:用户与团队之间的持久关系,包含 role;
- Socket:某个浏览器当前建立的临时连接。
一个 User 可以有多条 TeamMember,也可以同时有多个 Socket。关闭浏览器只会删除连接,不会删除成员关系;退出团队只会改变数据库关系,不代表必须注销账号。把这四个对象分清,很多设计错误会自然消失。
5. 为什么事件名使用过去式
team.membership.created 表示"成员关系已经创建",而不是一个待执行命令。只有新记录保存成功才发布。目标用户原本就是成员时,接口可以幂等返回已有记录,但不能再次发布 created。
合理的 payload 包含 eventId、occurredAt、team 摘要和 membership 摘要。eventId 用于去重,其他字段足以生成可读通知;完整团队与项目树仍由 REST 返回。
6. 在线、离线和重连
- Bob 在线:事件到达,立即展示通知并刷新列表;
- Bob 离线:当前 best-effort 方案不保存离线 toast,但下次登录的 GET /api/teams 会恢复业务状态;
- Bob 重连:主动同步团队快照,补偿断线期间可能漏掉的事件。
7. 失败发生在不同位置时怎样处理
| 失败点 | 数据库是否已有成员 | 是否应该发布 | 用户最终怎样恢复 |
|---|---|---|---|
| 权限或参数校验失败 | 否 | 否 | Alice 看到明确 HTTP 错误 |
| INSERT 失败 | 否 | 否 | 不显示成功,修复数据库问题 |
| INSERT 成功、emit 同步异常 | 是 | 尝试过但未成功 | Bob 下次 GET 恢复,不能回滚成员 |
| Bob 离线、空房间 emit | 是 | 服务端已 best-effort 调用 | Bob 下次登录 GET 恢复 |
| Bob 收到事件、GET 失败 | 是 | 已发送 | 页面提示同步失败,可重试或刷新 |
| 事件重复到达 | 是 | 已发送 | eventId 去重,只需一次回源 |
这张表说明错误处理也必须服从"数据库事实优先"。实时层失败不应把成功写入变回失败,前端回源失败也不应伪装成用户没有加入团队。
8. 为什么不直接把 payload push 进数组
Socket.IO 会保持当前连接内已送达 packet 的顺序,但连接中断时事件可能缺失,payload 也不包含页面所需的所有字段。直接把 payload 当完整状态,无法修复漏失,也可能因业务层重复发送而重复插入。当前 eventId 去重属于防御性保护;若未来引入重试、Outbox 或多个消息来源,它会更重要。页面可以立即展示 toast,团队卡片则在 REST 返回后更新,这是有意的职责分离。
9. 当前一致性边界
当前方案保证数据库成功后才尝试发布、重复邀请不重复写入、已有成员不重复通知、漏掉消息仍能 REST 恢复。它不保证 emit 后浏览器一定收到、不保存离线通知,也不处理跨 API 实例房间共享和数据库与消息的原子提交。
若要求每条通知必达且可追踪,需要 Outbox、消息队列、消费确认和持久化通知中心;作品 Demo 没有盲目引入这些复杂度。
10. 常见错误设计
- 让邀请者浏览器直接向 Bob 发消息:浏览器不应决定他人权限;
- 写库失败仍发送成功事件:事件必须位于成功分支;
- Bob 离线就不能邀请:在线状态不应阻止业务事实写入;
- 只在首次 connect 拉数据:重连后必须补一次快照。
11. 本篇小结
实时邀请不是用 Socket.IO 替代 REST,而是在 REST 业务链路外增加低延迟提醒:先写数据库,后发事件;事件让界面回源,REST 返回最终状态。