feat: add workbench kafka migration support

This commit is contained in:
richjohnrambo
2026-09-13 22:17:02 +08:00
parent c46cb4b382
commit 67e8c2c6f6
211 changed files with 7391 additions and 478 deletions
@@ -0,0 +1,488 @@
# IM 多开与跨 Pod 消息投递改造方案
> 日期:2026-09-12
> 适用范围:IMServer、Admin Server / HotGo、Redis、Kafka、JPush
> 关联文档:`docs/对接文档最新/IM-前后端对接文档.md`、`docs/IM-服务端架构与新功能开发指南.md`、`docs/IM-Redis三层数据分层改造方案.md`
## 1. 背景与目标
多开指同一用户在多设备、多 App 实例、多浏览器窗口或同设备多连接同时在线。服务端必须保证任意一个 Pod 收到的实时消息、业务通知、在线状态变化和账号治理命令,都能正确投递到同一租户内该用户在所有相关 Pod 上的 WebSocket / Long Polling 连接。
当前代码已有以下基础:
- `SessionStore` 管理本 Pod 内的实时连接。
- Redis presence 记录用户在线状态、session 集合和节点维度在线用户。
- 群消息已有 `redis_fanout.go`,通过 Redis Pub/Sub 通知其他 Pod。
- 群消息已拆到 `im_group_messages`,并具备 Redis seq、`client_mid` 幂等和 Kafka 异步持久化基础。
- `im_business_events.go` 仍使用租户库 business event outbox 做本地轮询投递。
本功能仍处于开发完善阶段,尚未上线。本次改动按“完整补齐多开跨 Pod 能力”处理,不按线上热修补或灰度兼容旧缺陷的思路处理。
本次改造目标:
1. 不再依赖租户库 business event outbox 作为 C 端实时通知主链路。
2. 建立统一的跨 Pod 实时投递总线,覆盖个人消息、群消息、系统消息、好友申请、好友状态、群治理、账号治理等需要多开同步的数据。
3. IMServer 只把本 Pod 连接保存在内存,全局在线、节点路由和跨 Pod 投递统一依赖 Redis。
4. Admin Server 不直连 IM 租户库,不直接操作用户 WS;后台写操作通过 Kafka Command 到 IMServer,由 IMServer 更新租户库并通过实时投递总线通知在线端。
5. 离线 Push 只做提醒和唤醒,不作为消息正文或业务状态真源。
## 1.1 前置确认
实施前必须确认生产部署是否启用 Tinode 原生 `cluster_config`:
- 如果未启用,Redis realtime dispatcher 是跨 Pod 实时投递唯一主链路。
- 如果启用,Topic 消息不要同时走 Tinode 原生 Topic master/proxy 和 Redis 广播式 dispatcher,否则可能重复投递。当前代码实现策略是:`globals.cluster != nil` 时,Topic 消息 fanout 不再额外发布 Redis realtime;session control 仍可通过 Redis 做幂等控制补充。
未启用原生 cluster 时,每个 Pod 必须配置唯一节点 ID。优先级为:
1. `node_id` 配置项。
2. `IM_NODE_ID` 环境变量。
3. `HOSTNAME` 环境变量。
4. 本地开发兜底值 `local`。
多 Pod 环境不能让所有实例都使用 `local`,否则 Redis fanout 会误认为其他 Pod 是本节点并跳过投递。
## 2. 核心结论
跨 Pod 实时投递使用 Redis Pub/Sub 或 Redis Stream,不使用数据库 outbox,也不使用 Kafka 作为 WebSocket fanout 主链路。
Kafka 仍保留以下职责:
- Admin Server 到 IMServer 的后台写命令。
- IMServer 到 Admin Server 的命令结果、审核结果、摘要同步。
- 私聊/群聊消息异步持久化。
- 文件安全检测、离线 Push 任务、审计、死信。
Redis 实时投递总线负责:
- 这个 Pod 收到消息后,通知其他 Pod。
- 其他 Pod 根据本地 `SessionStore` 找到真实 WS 连接并投递。
- 通过事件 ID / 消息 ID / `topic+seq` 做短期去重,优先复用已有 `ClaimShortState` / `ClaimShortStateValue` Redis 原语。
MySQL 负责:
- 最终状态和历史补偿。
- 用户、好友、群、系统通知、消息、文件、设备会话等可追溯数据。
- 客户端重连后按 `topic+seq`、通知 ID 或业务 ID 拉取缺口。
## 3. 总体架构
```text
Client A
-> Pod-1 WebSocket
-> Session / Topic / HTTP handler
-> 权限、幂等、seq、状态写入
-> 投递本 Pod 本地 sessions
-> Redis realtime bus
channel/stream: im:{tenant_id}:rt:{kind}
payload: envelope + server message
-> Pod-2 / Pod-3 subscriber
-> 按 tenant + target user/topic 找本 Pod sessions
-> queueOut 到对应 WS / Long Polling
Client B reconnect
-> hi -> token login -> sub me/topics
-> 按 seq/read/recv 或 notification cursor 从 MySQL/Redis 热缓存补齐
```
Admin Server 写操作:
```text
Admin Server
-> 校验后台权限
-> 写 hotgo 命令状态
-> Kafka im.admin.command
-> IMServer 任一 Pod 消费
-> 更新租户 MySQL / Redis / Topic 权限快照
-> Redis realtime bus 通知在线 C 端
-> Kafka im.admin.command.result 回写 Admin
```
## 4. 跨 Pod 实时投递总线设计
### 4.1 统一 Envelope
所有需要投递到其他 Pod 的实时事件统一包装:
```json
{
"event_id": "rt_01K...",
"tenant_id": "1",
"tenant_code": "default",
"kind": "message|topic_event|user_event|presence|session_control",
"target_type": "user|topic|device|session",
"target": "usr_xxx 或 grp_xxx 或 p2p_xxx",
"from": "usr_sender",
"source_node": "im-pod-1",
"skip_sid": "sid_sender",
"dedup_key": "message:grp_xxx:123",
"occurred_at": "2026-09-08T10:00:00.000Z",
"payload": {}
}
```
字段约定:
- `event_id`:全局唯一,消费者短期去重。
- `tenant_id`:必填,Redis key 和投递过滤必须带租户。
- `kind`:决定消费者如何解释 payload。
- `target_type=user`:投递给某用户在本 Pod 的所有连接。
- `target_type=topic`:投递给本 Pod 已订阅该 Topic 的连接。
- `target_type=device`:投递或控制某用户指定设备连接。
- `target_type=session`:精确控制某个 Session,主要用于本 Pod 本地,跨 Pod 可选。
- `skip_sid`:避免给发起连接重复投递,除非业务要求多端含当前端也收到。
- `dedup_key`:短期幂等键,例如 `msg:topic:seq`、`friend_request:id:version`。
- `payload`:可以是 Tinode `ServerComMessage` 的 JSON,也可以是业务刷新事件。
### 4.2 Redis Channel 建议
首版可以使用 Pub/Sub,要求客户端重连补偿兜底:
```text
im:{tenant_id}:rt:user
im:{tenant_id}:rt:topic
im:{tenant_id}:rt:presence
im:{tenant_id}:rt:session_control
```
如果需要更强的可观测和短暂重放,可升级为 Redis Stream:
```text
im:{tenant_id}:stream:rt
consumer_group = imserver:{node_id}
```
选择建议:
- 在线消息 fanout:Pub/Sub 足够,丢失后客户端按 seq 补偿。
- 踢设备、封禁、强制退出:建议 Pub/Sub + Redis control key 双写,避免订阅瞬断导致控制命令遗漏。
- Admin 重要业务变更:状态已落 MySQL,实时事件可以 Pub/Sub;客户端重进页面重新 GET。
### 4.3 本 Pod 投递规则
收到跨 Pod envelope 后:
1. 丢弃 `source_node == currentNodeID()` 的回环事件,除非显式需要自消费。
2. 使用 Redis `rt:dedup:{event_id}` 或本地 LRU 做短期去重。
3. 校验 `tenant_id`,不得跨租户投递。
4. 根据 `target_type` 选择本地 sessions:
- `user`:`session.uid == target && session.tenantID == tenantID`
- `topic`:`session.hasSub(target) && session.tenantID == tenantID`
- `device`:`session.uid == uid && session.deviceID == device_id`
5. 对每个匹配 Session 调用 `queueOut`。
6. 队列满时记录指标,不阻塞 Redis 订阅协程;客户端靠重连补偿。
## 5. 哪些数据需要多开处理
| 类别 | 场景 | 是否需要跨 Pod 实时投递 | 目标 | 最终补偿来源 | 说明 |
| --- | --- | --- | --- | --- | --- |
| 个人消息 | P2P 文本、图片、语音、文件、名片、通话邀请消息 | 是 | P2P Topic 在线成员、发送人其他端 | 私聊消息表 / Topic seq | Pod-1 收到发送后,本 Pod 投递并通过 Redis 投递到接收人和发送人其他端所在 Pod。 |
| 群消息 | 群文本、媒体、文件、@、合并转发 | 是 | 群 Topic 在线成员 | `im_group_messages` / Redis 热消息 | 已有群 fanout 雏形,需要纳入统一 envelope 和节点去重。 |
| 发送回执 | `pub` ack、消息发送成功 seq | 当前连接本地为主,发送人其他端可选 | 发送人其他端 | 消息表 / seq | 当前连接必须收到 `ctrl 202`;其他端同步消息用 `{data}`,不依赖 ctrl。 |
| 送达/已读 | `note recv/read` | 是 | 对端所有在线端、本人其他端 | subscription 游标 | 多端 read/recv 取最大值,所有端刷新气泡状态和未读数。 |
| 正在输入 | `kp/kpa` 临时状态 | 是,但 TTL 短 | 对端在线端或群内订阅端 | 无 | 不落库,允许丢失,限频。 |
| 消息撤回/编辑 | recall、revision | 是 | Topic 在线成员 | 消息版本表 / 消息状态 | HTTP 成功后发 topic 事件,在线端刷新 `topic+seq+version`。 |
| 删除/清空记录 | 当前用户视图删除 | 是,用户维度 | 本人所有端 | 删除游标 / del 记录 | 只影响操作者多端,不影响对方。 |
| 系统消息 | 系统通知列表新增、已读 | 是 | 目标用户所有端 | `im_system_notifications` | 新增需要提醒所有端;已读要同步清红点。 |
| 好友申请 | 创建、同意、拒绝 | 是 | 申请人、被申请人所有端 | 好友申请表、系统通知表 | 刷新新朋友列表、bootstrap badge、会话 context。 |
| 好友关系 | 添加、删除好友、拉黑/取消拉黑、备注 | 是 | 双方或操作者所有端 | 好友/黑名单表 | 输入区权限、会话标题、通讯录和黑名单需要同步。 |
| 好友在线状态 | 上线、离线、最近活跃 | 是 | 订阅相关 P2P/me Topic 的在线端 | Redis presence | 不应扫库;presence 以 Redis TTL 为准。 |
| 群成员 | 入群、退群、踢人、邀请审批 | 是 | 群 Topic 在线成员、相关用户所有端 | 群成员表、订阅表 | 同步群人数、成员列表、会话权限和系统提示。 |
| 群角色 | 群主转让、管理员增删 | 是 | 群 Topic 在线成员、目标用户所有端 | 群成员角色表 | 必须刷新 Topic `perUser` 权限快照和 Redis 成员缓存。 |
| 群禁言 | 全员禁言、成员禁言/解禁 | 是 | 群 Topic 在线成员、被禁言用户所有端 | 群治理/禁言表 | 输入框权限不能只靠前端本地状态。 |
| 群公告 | 发布、更新、已读 | 是 | 群 Topic 在线成员;已读为本人所有端 | 群公告表、公告已读表 | 顶部入口、公告详情和红点同步。 |
| 群资料 | 名称、头像、简介 | 是 | 群 Topic 在线成员 | 群表 / Topic desc | 会话标题、列表摘要、成员页都要刷新。 |
| 群文件 | 上传登记、删除、审核结果 | 是 | 群 Topic 在线成员 | 群文件表、文件引用表 | 文件正文不走 Redis,只投递文件项变化事件。 |
| 会话设置 | 置顶、免打扰、群昵称、草稿 | 是,用户维度 | 本人所有端 | subscription/private 设置表 | 不影响其他用户。 |
| 未读数/入口红点 | bootstrap badge、会话 unread、系统通知 unread | 是 | 本人所有端 | seq/read、系统通知表、业务表 | 实时事件只触发刷新;服务端 GET 返回最终值。 |
| 设备会话 | 新设备登录、退出当前设备、踢设备、全端退出 | 是 | 被控制用户所有端或指定设备 | 设备会话表、Token auth_version、Redis control key | 需要跨 Pod 关闭 WS。 |
| 账号治理 | 封禁、解封、冻结、注销、密码重置 | 是 | 目标用户所有端 | 用户状态、auth_version | Admin command 触发,IM 执行后踢在线并清 Token。 |
| 安全风控 | 风险弹窗确认/取消 | 是,用户维度 | 操作者所有端 | 风控挑战表 / Redis action token | 防止多端同时继续敏感操作。 |
| 会议/工作台 | 会议通知、日报月报提醒、工作台红点 | 是 | 目标用户所有端 | 对应业务表 | 会议笔记不产生通知,但页面返回后主动刷新。 |
| 文件安全 | 审核通过/拒绝、文件失效 | 是 | 文件引用可见用户 | 文件元数据/引用表 | 通知在线端刷新消息卡片或文件列表。 |
| 离线 Push | 用户离线、后台或不可实时投递 | 不属于 WS 多开,但需去重 | JPush alias / registration_id | Push 任务表或 Kafka | 多设备允许同 alias,payload 只带定位字段。 |
## 6. IMServer 改造方案
### 6.1 新增 Realtime Dispatcher
建议新增模块:
```text
server/realtime_dispatcher.go
server/store/realtime_runtime.go
```
职责:
- 发布跨 Pod envelope 到 Redis。
- 订阅 Redis envelope 并投递到本 Pod `SessionStore`。
- 提供 `PublishToUser`、`PublishToTopic`、`PublishSessionControl` 等方法。
- 统一 `event_id`、`dedup_key`、`skip_sid`、租户校验和指标。
接口示意:
```go
type RealtimeTarget struct {
TenantID types.TenantID
Type string // user/topic/device/session
Target string
SkipSid string
}
func PublishRealtime(target RealtimeTarget, msg *ServerComMessage) error
func PublishRealtimeEvent(target RealtimeTarget, event IMRealtimeEvent) error
```
现有 `redis_fanout.go` 后续可以改为调用统一 dispatcher,保留兼容封装。
### 6.2 个人消息跨 Pod
当前群消息已有专用 fanout,个人消息也需要同等级处理。
发送流程:
1. WS 收到 P2P `pub`。
2. Session 完成租户、登录、Topic 归一化。
3. Topic 校验好友关系、黑名单、风控、文件 ACL。
4. 使用 `client_mid` 做幂等,生成 Topic seq。
5. 本 Pod 投递:
- 当前连接返回 `ctrl 202`。
- 发送人其他端收到同一 `{data}`。
- 接收人在本 Pod 的端收到 `{data}`。
6. Redis realtime bus 投递到其他 Pod:
- `target_type=topic`,`target=p2p...`
- 或拆成 `target_type=user` 给双方用户。
7. 异步持久化到 MySQL。
8. 不在线设备只生成 Push 任务,客户端重连按 seq 补齐。
关键点:
- `noecho=false` 时发送人多端应同步消息。
- `skip_sid` 只能跳过当前连接的重复 `{data}`,不能跳过发送人其他端。
- P2P 传入 `usr...` 时必须先归一化为真实 P2P Topic。
### 6.3 群消息跨 Pod
保留当前 Redis seq、`client_mid` 幂等、热消息和 `im_group_messages` 最终落库模型。
需要补强:
- `redis_fanout.go` 使用统一 envelope。
- Redis channel 从全局 `im:0:group_fanout` 调整为带租户或 envelope 必带 `tenant_id`。
- 投递时优先使用 `topic:{topic}:node:{node_id}:online_members`,避免所有 Pod 都扫描全部 session。
- 消费端支持 topic 已加载和未加载两种路径:
- Topic 已加载:投递到 `hub.routeSrv`,由 Topic 统一处理。
- Topic 未加载:直接扫描本 Pod 已订阅该 Topic 的 session 投递。
- Redis Pub/Sub 丢失不补发,客户端重连通过 `since=last_seq+1` 补齐。
### 6.4 业务状态事件跨 Pod
替换 `im_business_events.go` 的数据库 outbox 轮询模式。
HTTP handler 或 Store 写成功后:
1. 同步写 MySQL 最终状态。
2. 失效 Redis 缓存和 Topic 权限快照。
3. 调用 realtime dispatcher 发布事件。
4. 如需通知离线用户,投 Kafka Push 任务。
业务事件 payload 示例:
```json
{
"info": {
"topic": "usr_target",
"from": "usr_actor",
"what": "im",
"event": "friend.request.created",
"payload": {"request_id": "frq_1"},
"ts": "2026-09-08T10:00:00Z"
}
}
```
群事件:
```json
{
"info": {
"topic": "grp_xxx",
"from": "usr_actor",
"what": "group",
"event": "group.role.updated",
"payload": {"topic": "grp_xxx", "user_id": "usr_2", "role": "admin"},
"ts": "2026-09-08T10:00:00Z"
}
}
```
### 6.5 Session Control 跨 Pod
设备退出、踢设备、全端退出、密码重置、账号封禁必须跨 Pod 生效。
建议 Redis key:
```text
im:{tenant_id}:session_control:user:{uid}:version
im:{tenant_id}:session_control:device:{uid}:{device_id}:version
im:{tenant_id}:session_control:event:{event_id}
```
流程:
1. IMServer 或 Admin command 更新 MySQL 用户状态、设备会话状态或 `auth_version`。
2. 写 Redis control version,TTL 可 1 到 7 天。
3. 发布 `target_type=user/device` 的 `session_control` envelope。
4. 各 Pod 收到后调用 `SessionStore.EvictUser` 或 `EvictUserDevice`。
5. Session 心跳或上行业务包也检查 Redis control version,防止 Pub/Sub 丢失。
### 6.6 Presence 多开规则
Redis presence 是全局在线真源:
- 用户登录成功:增加 session,写 `presence:user:{uid}`、`presence:user:{uid}:sessions`、`presence:node:{node_id}:users`。
- 订阅群 Topic:写 `topic:{topic}:online_members` 和 `topic:{topic}:node:{node_id}:online_members`。
- 心跳:刷新 session TTL、user TTL、已订阅群成员 TTL。
- 断线:移除 session;最后一个 session 离线时标记 `online=0`。
- 节点异常:依赖 TTL 自动过期。
好友在线状态变化:
- 不扫描所有用户。
- 只通知与该用户有订阅关系或正在会话的在线端。
- 客户端展示最近在线状态时,HTTP/WS 查询均以 Redis presence 为准。
## 7. Admin Server 改造方案
### 7.1 Admin 不直接做 WS 投递
Admin Server / HotGo 只负责后台权限、配置、审核和命令生产,不直接连接 IMServer 内存态,也不直接写租户库。
Admin 写操作统一:
1. 校验后台管理员权限。
2. 写 hotgo 命令表,记录 `command_id/event_id/idempotency_key/actor/request_id`。
3. 发送 Kafka `im.admin.command`。
4. 等待 IMServer `im.admin.command.result` 更新命令状态。
### 7.2 Admin 命令触发的多开场景
| Admin 操作 | Kafka Command | IMServer 动作 | C 端实时投递 |
| --- | --- | --- | --- |
| 封禁用户 | `user.disable` | 更新用户状态、递增 `auth_version`、踢在线 | `session_control user.disabled` 给目标用户所有端 |
| 解封用户 | `user.enable` | 更新用户状态、失效缓存 | 可选 `user.enabled` 给本人和相关管理视图 |
| 重置密码 | `user.password.reset` | 更新密码、递增 `auth_version`、撤销 Token | `session_control security.password_reset` |
| 踢设备 | `user.device.revoke` | 撤销设备会话 | `session_control device.revoked` |
| 解散群 | `group.delete` | 解散 Topic、撤销订阅和二维码 | `group.deleted` 给群在线成员 |
| 踢群成员 | `group.member.remove` | 更新成员状态和 Topic 订阅 | `group.member.removed` 给群成员和被踢用户多端 |
| 设置管理员 | `group.admin.set` | 更新角色、刷新权限快照 | `group.role.updated` |
| 删除管理员 | `group.admin.remove` | 降级角色、刷新权限快照 | `group.role.updated` |
| 禁言成员 | `group.member.mute` | 写禁言表、刷新权限 | `group.mute.updated` |
| 撤回/处置消息 | `message.moderate` | 更新消息状态、审核记录 | `message.recalled` 或 `message.moderated` |
| 文件审核处置 | `file.audit.update` | 更新文件状态和引用可见性 | `file.unavailable` 给引用可见用户 |
| 举报处理结果 | `abuse.report.resolve` | 更新举报状态、生成系统通知 | `system_notification.created` 给举报人 |
### 7.3 Admin 摘要同步
Admin 页面需要列表、统计和审核台数据,不应通过实时总线获得。
推荐:
- Admin 查询租户详情:调用 IM 内部只读 API。
- Admin 高频列表:维护 hotgo 摘要表。
- IM 状态变更后通过 Kafka result/event 同步摘要给 Admin。
这些摘要同步不属于 C 端多开实时投递,不要求发到用户 WS。
## 8. Redis、Kafka、MySQL 边界
| 组件 | 用途 | 不应用于 |
| --- | --- | --- |
| Redis Pub/Sub / Stream | 在线跨 Pod WS 投递、session control、presence 变化、短期去重 | 最终历史、长事务、后台命令可靠执行 |
| Redis KV/Set/Hash | presence、节点在线用户、Topic 在线成员、短期幂等、热消息 | 长期业务主数据 |
| Kafka | Admin command/result、消息持久化、Push、文件安全、审计、死信 | WebSocket 实时 fanout、验证码、在线状态 |
| MySQL 租户库 | 最终状态、历史查询、重连补偿、审计 | 在线连接表、实时 fanout |
| hotgo | Admin 后台主数据、配置、命令状态、摘要 | C 端 IM 消息和租户业务最终状态 |
## 9. 渐进实施计划
### 阶段一:统一跨 Pod 投递
1. 新增 realtime dispatcher。
2. 先增加 P2P 个人消息跨 Pod 投递。当前 P2P 是完全空白,应优先于群消息迁移。
3. 将现有群消息 Redis fanout 改为 dispatcher,并修复 `ServerComMessage` 内部路由字段 `json:"-"` 导致跨 Pod 丢 `TenantID/RcptTo/SkipSid` 的问题。
4. 增加用户维度事件投递,先覆盖好友申请、系统通知、已读同步。
5. 增加 session control 跨 Pod 踢设备/全端退出。
6. 同步上线 Redis 订阅健康检查、发布/消费/队列满指标和基础告警。
### 阶段二:替换 business event outbox
1. 已停止把 C 端在线通知写入 `im_business_events_outbox`,本次改造直接替换为 realtime dispatcher。
2. `publishIMRelationEvent`、`publishIMMeetingEvent` 等改为:
- 需要历史展示的系统通知写 `im_system_notifications`。
- 在线刷新直接调用 realtime dispatcher。
- 离线提醒投 Kafka Push。
3. 已移除 `startIMExternalBusinessEventPoller`。
4. `registration.invite_used` 改为注册提交后直接同步邀请码使用聚合;`security.password_reset` 改为密码重置后直接跨 Pod 踢线。
### 阶段三:Admin 联动
1. Admin Server 后台写操作全部通过 Kafka command。
2. IMServer command consumer 执行业务状态更新。
3. IMServer 执行成功后通过 realtime dispatcher 通知 C 端。
4. IMServer 通过 Kafka result/event 回写 Admin 命令状态和摘要。
### 阶段四:可观测和压测
1. 完善跨 Pod 投递指标:发布数、消费数、投递 session 数、丢弃数、队列满数、延迟。
2. 完善 Redis 订阅健康检查和告警。
3. 压测同一用户 10 端、多 Pod P2P 消息、千人群/万人群消息。
4. 验证 Pub/Sub 丢失后重连补偿路径。
## 10. 验收用例
| 用例 | 步骤 | 预期 |
| --- | --- | --- |
| 同用户多端收个人消息 | 用户 A 两端分别连 Pod-1、Pod-2,用户 B 发 P2P 消息到 Pod-1 | A 两端都收到消息,B 其他端也同步,消息只落一条。 |
| 群消息跨 Pod | 群成员分布在 Pod-1、Pod-2、Pod-3,Pod-1 收到群消息 | 所有在线订阅成员实时收到;离线成员重连按 seq 补齐。 |
| 重复发送幂等 | 断线后使用同一 `client_mid` 重发 | 返回同一 `seq`,不重复落库,不重复 Push。 |
| 已读多端同步 | A 在手机读到 seq=100 | A 其他端未读清除,B 多端看到 read 更新。 |
| 好友申请多端同步 | B 向 A 发好友申请 | A 所有在线端刷新新朋友红点和申请列表。 |
| 系统通知已读同步 | A 在 Pod-1 上把系统通知标记已读 | A 在 Pod-2 的端红点同步消失。 |
| 群角色变更 | Admin 将 A 设为管理员 | A 所有端群管理入口出现;群在线成员看到角色标识刷新。 |
| 禁言即时生效 | Admin 禁言 A,A 另一个端在 Pod-2 继续发消息 | 服务端返回 `GROUP_MUTED`,不能成功发送。 |
| 踢设备 | Admin 踢 A 的 device-1 | device-1 在任意 Pod 的连接被关闭,其他设备保持在线。 |
| 全端退出 | 密码重置或封禁 A | A 所有 Pod 上的连接关闭,旧 Token 后续登录失败。 |
| Pub/Sub 瞬断补偿 | 人为断开 Pod-2 Redis 订阅后发送消息,再恢复并重连客户端 | 实时期间可能漏收,但重连后按 seq 补齐,最终状态正确。 |
## 11. 代码落点建议
IMServer:
- `server/realtime_dispatcher.go`:统一跨 Pod 发布与订阅。
- `server/redis_fanout.go`:改造成群消息 dispatcher 适配层。
- `server/sessionstore.go`:补充按租户、用户、设备、Topic 的本地投递辅助方法。
- `server/session.go`:登录、心跳、断线时刷新 Redis presence 和 control version 检查。
- `server/topic.go`:个人/群消息发布成功后调用 dispatcher。
- `server/im_http.go`:好友、系统通知、群治理、会话设置等 HTTP 写成功后调用 dispatcher。
- `server/kafka_admin_commands.go`:Admin command 执行成功后调用 dispatcher。
- `server/store/group_message_runtime.go`:补充 realtime bus Redis 发布、订阅、去重 key。
Admin Server / HotGo:
- 后台写操作保留 command/outbox 到 Kafka 的可靠链路。
- 不新增任何直接调用 IM Pod WS 内存态的接口。
- 命令结果和摘要通过 Kafka result/event 回写 hotgo。
- 后台实时页面如需刷新,使用 Admin 自己的 WebSocket/SSE,从 hotgo 命令结果或摘要表驱动,不复用 C 端 WS。
## 12. 风险与注意事项
1. Pub/Sub 不保证离线重放,因此所有实时投递都必须有最终补偿来源。
2. `event_id` 去重不能代替消息 `client_mid` 幂等;二者层级不同。
3. 群消息 seq 只能有一个发号源,Redis 不可用时不能临时改用 MySQL 发号。
4. Session control 不能只靠 Pub/Sub,必须在心跳/上行业务包检查版本兜底。
5. Topic 权限变化后必须刷新内存快照和 Redis 缓存,否则多开端会出现权限漂移。
6. Push payload 不得带敏感正文,只带 `tenant_code/topic/seq/notification_id` 等定位字段。
7. Admin command 成功但实时投递失败时,不回滚业务状态;客户端通过重连或重新 GET 补偿。
8. 跨租户 Redis key、channel、payload 都必须带 `tenant_id`,不允许使用裸 `topic` 做全局路由。