# 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` 做全局路由。