Files
im_server/docs/IM-多开跨Pod消息投递改造方案.md
T

489 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` 做全局路由。