Files
im_server/docs/IM-Redis三层数据分层改造方案.md
T

30 KiB
Raw Blame History

IM Redis 三层数据分层改造方案

日期:2026-08-25

1. 背景与目标

当前 IM 服务需要接入 Redis,但不希望出现“进程内缓存一份、Redis 缓存一份、MySQL 再存一份”的重复状态,避免后续一致性维护困难。

本方案按业务数据特征拆成三层:

  • 进程内:只放低频变更配置和本节点运行态。
  • Redis:放小体量、高频读取、需要跨节点一致的热数据。
  • MySQL:放最终持久化数据、事务数据、审计数据和大体量列表。

第一阶段不把消息正文作为 Redis 的长期存储。万人大群可以把 24 小时内热消息放入 Redis 作为读加速,私聊消息最终来源是租户库 im_messages,群聊消息最终来源是租户库 im_group_messages。

2. 核心原则

  1. 同一类业务状态只确定一个主读取层,不在进程内和 Redis 同时维护两套可变状态。
  2. 进程内缓存只承载低频配置或本机连接态,不承载跨节点业务状态。
  3. 在线、离线、最近活跃等跨节点状态统一从 Redis 查询。
  4. MySQL 仍是需要持久化、事务和审计的数据最终来源。
  5. Redis 缓存 MySQL 数据时采用 cache-aside 模式,写入以 MySQL 事务成功为准。
  6. Redis key 必须带租户标识,虽然业务库是一租户一库,但 Redis 是共享基础设施,需要避免不同租户数据串扰。

3. 三层数据分类

层级 数据类型 典型数据 读写特点 一致性要求
进程内 静态或低频配置、本机运行态 系统配置、字典、租户配置、OSS/短信/内容安全/声网配置、本机 WebSocket 连接、topic channel 启动加载,定时刷新;连接态只在本机维护 配置允许秒级延迟;连接态只代表本节点
Redis 小体量热数据、跨节点状态、短期状态 用户在线状态、用户基础信息、群基础信息、群成员集合、好友/黑名单集合、24 小时热消息、验证码、临时 token、限流计数 高频读,TTL 或主动失效 跨节点查询一致,允许短暂最终一致
MySQL 持久化和事务数据 消息、文件记录、群申请审批、举报、审核任务、操作日志、账务类数据、大分页列表 事务写入,分页查询,审计追溯 最终权威来源

4. 进程内层设计

4.1 适合放进程内的数据

  • 系统配置:后台 hotgo 中的系统参数。
  • 字典配置:hg_sys_dict_type、hg_sys_dict_data 等字典项。
  • 租户配置:租户启停、基础配置、默认策略。
  • 第三方配置:OSS、阿里云内容安全、短信、声网等接入参数。
  • 本节点运行态:当前进程内的 WebSocket session、local topic channel、本机订阅关系、本机连接数。

4.2 刷新策略

  • 服务启动时加载一次配置。
  • 定时刷新,建议 60 秒。
  • 后台配置变更后,后续可补充主动失效或广播刷新。
  • 配置读取失败时保留旧值,并打印告警日志。

4.3 边界说明

进程内层不保存用户在线状态的全局结果。进程内只知道“本节点有哪些连接”,跨节点判断用户是否在线必须查 Redis。

5. Redis 层设计

5.1 第一阶段必须接入

用户在线状态

在线状态是 Redis 第一优先级改造内容。原因是 WebSocket 长连接固定落在某个节点上,本机内存可以管理本机连接,但其他节点无法直接知道该用户是否在线。

建议 key:

im:{tenant_id}:presence:user:{uid}
im:{tenant_id}:presence:user:{uid}:sessions
im:{tenant_id}:presence:node:{node_id}:users

presence:user:{uid} 建议使用 hash:

online = 1
node_id = 当前节点 ID
session_count = 当前会话数
last_active_at = 最近活跃时间
last_seen_at = 最近可见时间
device = 设备类型
ua = 客户端 UA

生命周期:

  1. WebSocket 登录成功后写入 Redis 在线状态。
  2. 心跳或业务包刷新 TTL。
  3. 断开连接时扣减 session 数,最后一个 session 断开后标记离线。
  4. 节点异常退出时依赖 TTL 自动过期,避免永久在线。
  5. 其他节点查询用户在线状态时只查 Redis。

TTL 建议:

  • 在线状态 hash:60 到 120 秒。
  • session 集合:60 到 120 秒,随心跳刷新。
  • node 用户集合:可不强依赖 TTL,用于辅助排查和节点清理。

当前代码已经接入基础 PresenceStore:

  • 登录成功后写入 presence:user:{uid}、presence:user:{uid}:sessions、presence:node:{node_id}:users。
  • 客户端每次上行业务包刷新 last_active_at 和 TTL。
  • session 清理时移除当前 session,最后一个 session 离线后将 online 置为 0。
  • 对外提供 store.GetUserPresence 作为后续用户在线状态查询入口。
  • 群 Topic 在线成员已同步维护 topic:{topic}:online_members 和 topic:{topic}:node:{node_id}:online_members,订阅/离开时更新,客户端心跳或任意上行业务包会刷新已订阅群的在线成员 TTL。
  • 订阅列表中的用户在线状态已改为 Redis 优先读取;Redis 未启用或读取失败时,回退到原有进程内在线判断。

短期状态

以下数据适合放 Redis,减少 MySQL 压力并天然支持过期:

  • 注册验证码、登录验证码。
  • 邀请码短期校验结果。
  • 企业码登录临时 token。
  • 登录失败次数、风控计数、接口限流计数。
  • 幂等 key、短期操作锁。

5.2 第二阶段建议接入

用户基础信息

适合缓存:

  • 用户昵称、头像、状态签名。
  • 用户公开资料。
  • 用户账号到 UID 的映射。

不建议缓存:

  • 密码 hash。
  • 高敏认证字段。
  • KYC 原始材料。

读写规则:

  1. 查询先读 Redis。
  2. 未命中读 MySQL。
  3. MySQL 命中后写入 Redis。
  4. 用户资料变更时先提交 MySQL,再删除或更新 Redis。

群相关数据

群数据体量相对可控,且聊天过程中读取频繁,适合逐步放入 Redis:

  • 群基础信息:群名、头像、简介、人数、群类型。
  • 群配置:入群审批、禁言策略、邀请权限、公告权限。
  • 群成员集合:成员 UID 集合。
  • 群成员角色:群主、管理员、普通成员。
  • 群成员状态:禁言、移出、黑名单。

建议 key:

im:{tenant_id}:group:{group_id}:profile
im:{tenant_id}:group:{group_id}:settings
im:{tenant_id}:group:{group_id}:members
im:{tenant_id}:group:{group_id}:admins
im:{tenant_id}:group:{group_id}:muted

关系数据

好友、黑名单、关注类关系适合用 Redis set 承载热读:

im:{tenant_id}:relation:friends:{uid}
im:{tenant_id}:relation:blocklist:{uid}

用于快速判断:

  • 是否好友。
  • 是否被拉黑。
  • 是否允许发起会话。
  • 是否允许查看在线状态或最近在线时间。

5.3 第三阶段可选接入

  • 首页 bootstrap 聚合结果:短 TTL 缓存,减少多表聚合查询。
  • 群成员分页快照深度优化:基础 HTTP 结果快照已接入,后续大群可继续补 sorted set、稳定游标和主动失效。
  • 未读数深度优化:基础总未读 Redis 热缓存已接入,后续可继续补 Topic 维度未读计数、bootstrap 聚合缓存和主动失效事件。

5.4 万人大群实时推送专项

万人大群的实时推送需要和 Redis 改造一起考虑。核心目标是避免普通小群模式下的全量 fanout 压垮服务端,同时保证在线用户实时可达、离线用户通过极光 Push 提醒、客户端重连后可按 seq 补齐消息。

该部分作为 Redis 改造的近期实现项处理,不再仅作为未实现增强项记录。

数据分层

层级 承载内容 说明
进程内 本节点在线连接、本节点 topic 订阅、本节点待发送队列 只负责当前节点连接和发送,不保存全局群成员在线状态。
Redis 群在线成员索引、用户所在节点、群分片、原子 seq 发号器、24 小时热消息、短期去重 key 用于判断哪些在线用户需要实时推送、原子获取群消息 seq,以及近期高频消息读取加速。
MySQL 租户库 im_group_messages 群消息正文、群消息 seq 结果、消息索引、离线同步游标、审计数据 作为群消息最终存储,保存 Redis 分配后的 seq,不负责实时发号。
极光 Push 离线提醒 只做提醒,不承载消息正文最终一致性。

建议 Redis key

im:{tenant_id}:presence:user:{uid}
im:{tenant_id}:topic:{topic}:online_members
im:{tenant_id}:topic:{topic}:node:{node_id}:online_members
im:{tenant_id}:topic:{topic}:shard:{shard_id}:members
im:{tenant_id}:topic:{topic}:last_seq
im:{tenant_id}:topic:{topic}:hot_msg_seqs
im:{tenant_id}:topic:{topic}:hot_msg:{seq}
im:{tenant_id}:topic:{topic}:uniq:msg:{from_uid}:{client_msg_id}
im:{tenant_id}:push:dedup:{topic}:{seq}:{uid}

说明:

  • topic:online_members 保存当前群在线成员集合,可由用户上线、订阅群、退订群、断开连接维护。
  • topic:node:{node_id}:online_members 用于跨节点推送时按节点拆分,减少广播范围。
  • topic:shard:{shard_id}:members 可用于万人群成员分片,后续支持分批扫描和补偿。
  • topic:last_seq 是群消息 seq 发号器,使用 Redis INCR 或 Lua 脚本原子获取下一个 seq。
  • topic:hot_msg_seqs 使用 sorted set 保存 24 小时内热消息 seq,score 为 seq 或发送时间。
  • topic:hot_msg:{seq} 保存单条热消息正文快照,TTL 建议 24 小时。
  • topic:uniq:msg:{from_uid}:{client_msg_id} 保存群消息发送唯一 key,用于客户端重试幂等和服务端去重。
  • push:dedup 用于极光 Push 或实时推送去重,TTL 建议按分钟级设置。

唯一 key 与幂等规则

Redis 唯一 key 用于挡住短时间内的重复提交,MySQL 唯一索引作为最终兜底。不能只依赖 Redis 做永久唯一性,因为 Redis key 会过期,也可能被运维清理。

群消息发送唯一 key:

im:{tenant_id}:topic:{topic}:uniq:msg:{from_uid}:{client_msg_id}

建议 value:

{
  "status": "pending|done|failed",
  "seq": 12345,
  "message_id": 987654321,
  "created_at": "2026-08-25T12:00:00.000+08:00"
}

处理规则:

  1. 客户端必须传 client_msg_id,同一发送人、同一 Topic 内唯一。
  2. 服务端收到群消息后先用 SET key value NX EX ttl 抢占唯一 key,初始状态为 pending。
  3. 抢占成功才继续 Redis INCR 获取群 seq。
  4. 抢占失败时读取 key:
    • done:直接返回已生成的 seq/message_id。
    • pending:返回处理中或短暂等待后重试,避免重复发号。
    • failed:允许客户端用同一个 client_msg_id 重试,或要求重新生成 client_msg_id。
  5. 消息写入 im_group_messages 成功后,将唯一 key 更新为 done,记录 seq/message_id。
  6. 消息写入失败时,将唯一 key 更新为 failed 并设置较短 TTL;已经获取的 seq 不回退,允许空洞。
  7. im_group_messages 仍保留 UNIQUE(topic, from_user, client_msg_id),防止 Redis key 过期后重复落库。

TTL 建议:

  • pending:30 到 120 秒。
  • done:24 小时到 7 天,按客户端重试窗口决定。
  • failed:5 到 30 分钟。
  • Push 去重 key:5 到 30 分钟。

Seq 发号规则

  • 群消息 seq 由 Redis 维护,不从 MySQL 查询最大 seq。
  • 每个 Topic 独立一个 topic:last_seq,使用 INCR im:{tenant_id}:topic:{topic}:last_seq 原子获取。
  • 只要求单 Topic 内单调递增,不要求全局连续。
  • 如果取到 seq 后消息持久化失败,该 seq 可以形成空洞;客户端按 seq 补拉时允许跳过服务端确认不存在的 seq。
  • Topic 创建或 Redis key 丢失时,当前代码使用 topic 内存态 lastID 作为 Redis 初始化基线;该值来自 topic 加载结果或当前进程已确认的最新 seq,然后再执行原子 INCR。
  • Redis 不可用时不能用数据库临时发号继续写大群消息,应返回可重试错误,避免多源发号造成 seq 冲突。

发送流程

  1. 服务端收到群消息后,先完成权限校验和禁言校验。
  2. 使用 Redis 唯一 key 抢占 topic + from_uid + client_msg_id,防止客户端重试重复发送。
  3. 抢占成功后,使用 Redis INCR 或 Lua 脚本从 topic:last_seq 原子获取下一个群 seq。
  4. 消息正文带着该 seq 写入租户库 im_group_messages。
  5. 落库成功后将唯一 key 更新为 done,记录 seq/message_id。
  6. 对万人大群或高热群,将消息快照写入 topic:hot_msg:{seq},并把 seq 加入 topic:hot_msg_seqs,TTL 保持 24 小时。
  7. 查询 Redis 中该群在线成员,按 node_id 分组。
  8. 本节点在线成员直接从进程内连接表推送。
  9. 其他节点在线成员通过 Redis Pub/Sub 通知目标节点推送;当前代码使用 im:0:group_fanout 广播精简后的 ServerComMessage,订阅节点收到后投递到本机 hub.routeSrv,只发送给本节点已加载 Topic 的在线连接。
  10. 不在线成员不做服务端全量离线消息 fanout,只更新离线游标或依赖消息 seq 补拉,并触发极光 Push。
  11. 客户端重连后按 Topic seq 拉取缺失消息,优先读 Redis 热消息,未命中再回源租户库 im_group_messages。

读取流程

客户端消息读取仍以 seq 为准,Redis 只优化 24 小时内的热读:

  1. 在线实时收到消息后,客户端本地落库并更新本地最大 seq。
  2. 重连、切换设备或发现 seq 不连续时,调用历史消息接口按 topic + after_seq 补拉。
  3. 服务端先按 seq 范围读取 Redis 热消息。
  4. Redis 命中的消息直接返回;缺失、过期或不连续的 seq 回源租户库 im_group_messages。
  5. 回源命中后可按策略重新写入 Redis,保持 24 小时 TTL。
  6. 极光 Push 唤醒后,客户端仍走正常登录和 seq 补拉链路。

热消息缓存边界

  • 只缓存 24 小时内高频读取的群消息,普通群可不开启或按群规模阈值开启。
  • Redis 中的消息快照不作为审计和历史同步最终来源。
  • 删除、撤回、编辑消息后,必须同步删除或更新对应 topic:hot_msg:{seq},避免客户端读到旧正文。
  • 大消息、文件二进制、音视频内容不直接放 Redis,只缓存消息元数据和 OSS object key。
  • Redis 容量按租户和群设置上限,超过阈值时优先淘汰低热群或旧 seq。
  • 当前代码只在 Redis 能完整命中一页群消息时直接返回;如果 Redis 缺失、过期、不连续或读取带有用户软删除视角,则回源 MySQL,避免缓存缺口导致消息缺失或读到用户已删除内容。

离线 Push 口子

极光 Push 只用于离线提醒,建议 payload 只放最小必要信息:

{
  "type": "group_message",
  "topic": "grp_xxx",
  "seq": 12345,
  "tenant_id": "tenant_xxx"
}

不建议在 Push payload 中放完整消息正文,避免隐私泄露、厂商通道截断和多端一致性问题。

一致性边界

  • Redis 的在线成员集合允许短暂不准,最终由心跳 TTL 和订阅变更修正。
  • Redis 唯一 key 负责短期幂等和去重,MySQL 唯一索引负责最终唯一性兜底。
  • seq 是否已分配以 Redis topic:last_seq 为准;群消息是否真实存在,以租户库 im_group_messages 为准。
  • Redis 发号成功但消息持久化失败时,允许 seq 空洞,不回退 topic:last_seq。
  • 实时推送失败不影响消息持久化,通过客户端 seq 补拉恢复;Redis 热消息缺失时回源租户库 im_group_messages。
  • 极光 Push 失败不影响消息存储,只影响离线提醒,需要记录失败并可观测。
  • Redis Pub/Sub 只负责在线实时投递,不作为可靠消息队列;节点短暂断线错过实时消息时,客户端仍以 Topic seq 补拉恢复。

6. MySQL 层设计

6.1 必须保留在 MySQL 的数据

  • 私聊消息正文、私聊消息索引、离线同步游标。
  • 群消息正文、群消息索引、群消息审计数据。
  • 文件记录、OSS object key、文件安全审核结果。
  • 群邀请、进群申请、审批记录。
  • 群公告、公告确认、公告历史。
  • 用户认证、KYC 记录、企业注册资料。
  • 举报、投诉、内容安全审核任务。
  • 操作日志、审计日志、管理后台变更记录。
  • 需要事务一致性的业务状态。

6.2 消息表拆分

当前 im_messages 后续只作为私聊、文件助手等非群聊消息的主存储。群聊消息新增租户库业务表 im_group_messages,避免所有消息挤在一张表里,也方便后续针对大群按 topic + seq、时间、热度做分区、归档和缓存。

建议表结构:

CREATE TABLE IF NOT EXISTS im_group_messages (
  id BIGINT NOT NULL AUTO_INCREMENT,
  topic VARCHAR(64) NOT NULL COMMENT '群 Topic,如 grp_xxx',
  group_id BIGINT NOT NULL COMMENT '业务群 ID,便于分区、治理和统计',
  seqid BIGINT NOT NULL COMMENT '群内消息序号,由 Redis 原子发号',
  from_user BIGINT NOT NULL COMMENT '发送人用户 ID',
  client_msg_id VARCHAR(64) NULL COMMENT '客户端幂等消息 ID',
  msg_type VARCHAR(32) NOT NULL DEFAULT 'text' COMMENT 'text/image/file/audio/video/system 等',
  status VARCHAR(32) NOT NULL DEFAULT 'normal' COMMENT 'normal/recalled/deleted/blocked',
  content JSON NULL COMMENT '消息正文或 Drafty 内容',
  head JSON NULL COMMENT '消息头,包含 @、引用、客户端能力等',
  mentioned_user_ids JSON NULL COMMENT '@ 用户列表,便于提醒和检索',
  mention_all TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否 @ 全体',
  reply_seqid BIGINT NULL COMMENT '回复的群消息 seq',
  root_seqid BIGINT NULL COMMENT '线程或合并上下文根消息 seq',
  file_count INT NOT NULL DEFAULT 0 COMMENT '附件数量',
  content_size INT NOT NULL DEFAULT 0 COMMENT '正文 JSON 序列化后的字节数',
  has_risk TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否命中风控或内容安全',
  audit_status VARCHAR(32) NOT NULL DEFAULT 'pass' COMMENT 'pass/pending/reject',
  version INT NOT NULL DEFAULT 1 COMMENT '编辑版本',
  recalled_at DATETIME(3) NULL,
  recalled_by BIGINT NULL,
  deleted_at DATETIME(3) NULL,
  expires_at DATETIME(3) NULL COMMENT '可选保留期,便于过期归档或清理',
  created_at DATETIME(3) NOT NULL,
  updated_at DATETIME(3) NOT NULL,
  PRIMARY KEY (id),
  UNIQUE KEY uk_im_group_messages_group_seq (group_id, seqid),
  UNIQUE KEY uk_im_group_messages_topic_seq (topic, seqid),
  UNIQUE KEY uk_im_group_messages_topic_sender_client_mid (topic, from_user, client_msg_id),
  KEY idx_im_group_messages_group_created (group_id, created_at),
  KEY idx_im_group_messages_group_status_seq (group_id, status, seqid),
  KEY idx_im_group_messages_topic_created (topic, created_at),
  KEY idx_im_group_messages_topic_from_seq (topic, from_user, seqid),
  KEY idx_im_group_messages_audit (audit_status, created_at),
  KEY idx_im_group_messages_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

设计说明:

  • seqid 使用 BIGINT,避免大群长期高频消息导致 INT 不够用。
  • group_id + seqid 和 topic + seqid 都保持唯一,业务治理优先用 group_id,协议补拉仍可用 topic。
  • client_msg_id 用于发送幂等,索引带上 topic,避免不同群复用客户端消息 ID 时互相冲突。
  • status 标记普通、撤回、删除、拦截状态,拉历史时可以返回占位或过滤。
  • mentioned_user_ids、mention_all 单独存,方便 @ 提醒、离线 Push 和后台治理,不必每次解析正文。
  • audit_status、has_risk 为内容安全和风控留字段,高风险消息可先 pending 后再放行。
  • reply_seqid、root_seqid 为引用回复、线程、合并转发上下文预留。
  • 文件二进制不进表,表内只保存消息内容和 OSS object key 等元数据;群消息附件会同步登记到 im_group_files,并记录 source_topic/source_seq 追溯来源消息。
  • 超大规模租户后续可按 group_id hash 分表,或按 created_at/expires_at 做归档分区。

读写规则:

  1. 私聊消息继续写 im_messages。
  2. 群聊消息写 im_group_messages。
  3. 群消息发号由 Redis topic:last_seq 原子递增完成,im_group_messages.seqid 只保存结果。
  4. Redis 24 小时热消息命中时直接返回;未命中按 topic + seqid 回源 im_group_messages。
  5. 撤回、编辑、删除群消息时,以 im_group_messages 为准,并同步更新或删除 Redis 热消息。

6.3 消息正文说明

第一阶段不把消息正文作为 Redis 长期存储。万人大群和高热群允许缓存 24 小时内热消息,作为客户端重连补拉、最近历史翻页和多端同步的读加速。

消息正文主要读取场景:

  • 新设备登录后的历史消息同步。
  • 离线用户重新上线后的消息补拉。
  • 客户端本地消息缺失后的重拉。
  • 举报、风控、内容安全复查。
  • 管理后台审计或客服处理。

这些场景的最终来源仍是 MySQL 的消息表。私聊回源 im_messages,群聊回源 im_group_messages。Redis 热消息只覆盖短期高频读取,命中则减少数据库压力,未命中必须回源。

7. 读写一致性规则

7.1 进程内配置

  • 进程启动加载配置。
  • 定时刷新配置。
  • 配置读取失败时使用旧值。
  • 配置变更允许短时间延迟生效。

7.2 Redis 运行态

  • 在线状态以 Redis 为跨节点查询来源。
  • 本机内存只用于管理本机连接。
  • Redis 写入失败时,在线状态降级为未知或离线,不阻断基础连接能力。
  • 心跳持续刷新 TTL,节点异常时依赖 TTL 自动回收。

7.3 Redis 缓存 MySQL 热数据

  • 读:先 Redis,未命中再 MySQL,最后回填 Redis。
  • 写:先 MySQL 事务提交,再删除或更新 Redis。
  • 消息 seq:群聊先通过 Redis INCR 或 Lua 脚本原子获取 seq,再带 seq 写租户库 im_group_messages;私聊仍按现有 im_messages 链路处理。
  • 消息热缓存:消息持久化成功后,再写 Redis 24 小时热消息;热消息写失败不影响消息发送成功。
  • Redis 故障:直接回源 MySQL,记录降级日志和指标。
  • 不允许因为 Redis 写失败回滚已经成功的 MySQL 业务事务,除非该业务本身强依赖 Redis 状态。

7.4 MySQL 持久化

  • 需要审计、事务、历史追溯的数据直接写 MySQL。
  • Redis 和进程内缓存只做读取加速或运行态存储。
  • 私聊消息使用 im_messages,群聊消息使用 im_group_messages,避免单表无限膨胀。

8. 租户隔离

当前方案是一租户一库,业务表中不再保留 tenant_id 字段。但 Redis、进程内配置、日志指标仍属于共享运行环境,必须保留租户维度。

Redis key 统一包含租户标识:

im:{tenant_id}:...

如果请求早期只有企业码或租户编码,应先解析到租户 ID 或租户库配置,再进入业务缓存读取。

9. 代码改造建议

9.1 基础设施

  • 增加 Redis 配置:地址、密码、DB、连接池、超时、key 前缀。
  • 增加 Redis client 初始化和健康检查。
  • 增加统一 key builder,避免业务代码手写 key。
  • 增加缓存指标和日志。

当前代码将 Redis 配置挂在 store_config.redis 下;不配置则不启用 Redis 群消息发号和唯一 key,群消息仍兼容旧的本机 lastID + 1 发号。

群消息物理表按自然月拆分时,落库路由、跨月查询、幂等和迁移方案见:IM-群消息按月分表改造方案。

配置示例:

{
  "store_config": {
    "redis": {
      "addr": "127.0.0.1:6379",
      "username": "",
      "password": "",
      "db": 0,
      "key_prefix": "im",
      "dial_timeout_ms": 2000,
      "read_timeout_ms": 2000,
      "write_timeout_ms": 2000,
      "pending_ttl_seconds": 120,
      "done_ttl_seconds": 86400,
      "failed_ttl_seconds": 600,
      "hot_message_ttl_seconds": 86400,
      "presence_ttl_seconds": 120,
      "cache_ttl_seconds": 300,
      "short_state_ttl_seconds": 600,
      "group_fanout_enabled": true
    }
  }
}

group_fanout_enabled 默认关闭。启用后,群消息本节点投递完成后会发布 Redis Pub/Sub,其他节点订阅后只向本节点已在线的同群连接推送;如果节点没有加载该 Topic,则读取 Redis 本节点群在线成员集合做兜底直投,本地会话订阅表作为兼容兜底,实时投递失败再由客户端 seq 补拉恢复。

当前第二阶段已接入:

  • 用户资料单对象缓存:store.Users.Get 优先读 Redis,用户更新/删除后失效。
  • Topic 单对象缓存:store.Topics.Get 优先读 Redis,Topic 更新、转让、删除后失效。
  • 单条订阅关系缓存:store.Subs.Get 优先读 Redis,订阅更新/删除后 active/any 两种缓存同时失效。
  • 群治理资料缓存:群设置 GroupBusiness.GetSettings、群成员 GroupBusiness.GetMember 优先读 Redis,更新、离群、角色变更后刷新或失效。
  • 群成员分页快照:GET /v1/im/groups/{topic}/members 按租户、Topic、操作者权限、筛选条件、cursor 和 limit 缓存响应快照,未命中回源订阅列表。
  • 用户总未读热缓存:store.Users.GetUnreadCount 先读 Redis,未命中回源 MySQL 并回填;在线用户 unread updater 更新内存计数时同步写 Redis。
  • 好友/黑名单 Redis Set 热缓存:关系上下文判断先查 friends:{uid}、blocklist:{uid} 正向 set,命中后减少 MySQL 查询;未命中仍回源 MySQL,新增/删除好友、拉黑/解除拉黑会同步更新 set。
  • 验证码短状态缓存:验证码创建、查询、验证、失败、消费会同步 Redis,GetActive 使用 scene + phone_hash 的 active key 做重发窗口热读。
  • Redis 频控:验证码创建按 tenant + scene + phone_hash 使用 Redis 原子 INCR + EXPIRE 做 1 小时 5 次限制,手机号只使用 hash,不写明文。
  • 动作 token 短状态缓存:创建时写 Redis,消费成功后删除缓存;最终消费仍以 MySQL 状态机为准,保留审计一致性。
  • SeqId 收口:消息、Topic 游标、订阅读/收游标、消息范围、收藏/举报/群文件引用、通话消息引用、JSON/gRPC 协议结构均改为 int64;MySQL 新建表 schema 中对应字段改为 BIGINT。
  • 进程内配置缓存:租户解析、注册短信开关、第三方配置读取已加本地 TTL 缓存,减少 HotGo 控制库频繁查询;缓存过期后按下一次读取刷新。

9.2 接口抽象

建议新增接口:

type PresenceStore interface {
    MarkOnline(ctx context.Context, tenantID string, uid string, sessionID string, meta PresenceMeta) error
    Refresh(ctx context.Context, tenantID string, uid string, sessionID string) error
    MarkOffline(ctx context.Context, tenantID string, uid string, sessionID string) error
    Get(ctx context.Context, tenantID string, uid string) (Presence, error)
}

type ConfigCache interface {
    GetDict(ctx context.Context, key string) (DictValue, bool)
    GetTenantConfig(ctx context.Context, tenantID string) (TenantConfig, bool)
    Refresh(ctx context.Context) error
}

type ReadModelCache interface {
    GetUserProfile(ctx context.Context, tenantID string, uid string) (UserProfile, bool)
    SetUserProfile(ctx context.Context, tenantID string, profile UserProfile) error
    DeleteUserProfile(ctx context.Context, tenantID string, uid string) error
}

9.3 业务接入顺序

  1. 先接入 Redis client 和配置。
  2. 接入在线状态 PresenceStore。
  3. 群消息接入 Redis SET NX 唯一 key 与 INCR 原子 seq 发号,群消息正文写入 im_group_messages。
  4. WebSocket 登录、心跳、断开链路写入 PresenceStore。
  5. 用户在线状态查询接口改为读 Redis。
  6. 配置和字典改为进程内 TTL 缓存刷新。
  7. 再逐步接入用户、群、好友关系热数据缓存。

10. 故障降级

场景 降级策略
Redis 不可用,查询在线状态 返回未知或离线,记录告警
Redis 不可用,读取用户/群缓存 回源 MySQL
Redis 不可用,验证码或短期 token 涉及安全的接口应失败关闭
Redis 不可用,万人大群发消息 无法原子获取群 seq,应返回可重试错误;不使用 MySQL 临时发号,避免 seq 冲突
配置刷新失败 使用进程内旧配置
MySQL 不可用 持久化业务失败,不依赖 Redis 兜底

11. 监控指标

  • Redis 连接状态。
  • Redis 命令耗时。
  • Redis 缓存命中率。
  • 在线用户 key 数量。
  • 心跳刷新失败次数。
  • Redis 回源 MySQL 次数。
  • 大群在线成员集合大小。
  • 大群按节点分发耗时。
  • 大群实时推送成功率和补拉率。
  • 极光 Push 触发数、成功数和失败数。
  • 配置刷新成功和失败次数。
  • Presence TTL 过期导致的离线数量。

12. 第一阶段落地范围

第一阶段建议只做以下内容:

  • Redis 基础配置和 client 初始化。
  • 用户在线、离线、心跳刷新、跨节点查询。
  • 验证码、临时 token、限流计数等短期状态迁移到 Redis。
  • 系统配置、字典、租户配置、第三方配置改为进程内 TTL 缓存刷新。
  • 万人大群实时推送纳入 Redis 改造近期实现范围,先完成 Redis key、在线成员索引、节点分片和跨节点实时分发链路。
  • 明确消息正文不进入 Redis 长期存储;万人大群 24 小时热消息可进入 Redis 做读加速。
  • 群成员分页快照和用户总未读 Redis 热缓存已完成基础接入。

后续再逐步推进好友/黑名单全量预热、群成员分页快照、未读数等缓存改造。