news 2026/7/2 10:56:42

Redis 支撑即时通讯源码在线状态与路由转发的实现思路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis 支撑即时通讯源码在线状态与路由转发的实现思路

IM 即时通讯的系统技术复杂度并不来自“有多少聊天功能”,而来自实时通信链路本身。

用户看到的是一条消息从输入框发出,服务端真正处理的是连接鉴权、协议解析、消息编号、幂等判断、消息落库、在线路由、跨节点转发、ACK 确认、离线同步、多端状态刷新等一整条链路。

如果系统同时覆盖 Android、iOS、H5、PC,并且后端基于SpringBoot + WebSocket + Socket + HTTP + Redis + MySQL 5.7+设计,那么架构重点就不能只停留在接口层,而要围绕“实时消息如何可靠流转”来建模。

本文从工程实现角度,分析一个 IM 即时通讯系统在消息链路、连接状态、存储索引、Redis 路由、多端一致性和集群部署中的技术设计思路。

从一次连接开始,而不是从一个接口开始

普通业务系统通常是“请求来了,接口处理,返回结果”。IM 系统不同,客户端和服务端之间需要长期保持连接。

一个客户端进入聊天系统后,第一步不是发送消息,而是建立实时通道。

这个通道可能来自 App 的 Socket 自定义协议,也可能来自 H5 或 PC 的 WebSocket。连接建立后,服务端需要完成几件事:

验证 token 是否有效。

识别当前用户 ID。

识别当前设备类型。

绑定当前连接所在节点。

记录连接最后活跃时间。

启动心跳检测。

这一步完成后,服务端才知道“某个用户的某个端,当前连接在哪台 IM 节点上”。

可以把这个过程理解成一张动态路由表:

userId=10001 android -> im-node-01 / channel-a81 h5 -> im-node-02 / channel-h19 pc -> im-node-03 / channel-p77

这个结构比简单的userId -> channelId更适合多端在线。否则用户在 PC 登录后,可能覆盖 App 的连接,导致移动端收不到消息或状态不同步。

Redis 在 IM 系统里不是普通缓存

在很多业务系统里,Redis 只是缓存热点数据。但在 IM 系统中,Redis 更像实时状态中心。

它保存的不是“查库结果副本”,而是系统当前运行状态,例如谁在线、在哪个节点、哪个会话有多少未读、哪个临时 token 还有效、哪个红包领取流程正在并发处理。

在线路由可以这样保存:

@Service public class ImConnectionRegistry { private final StringRedisTemplate redisTemplate; public ImConnectionRegistry(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } public void online(Long userId, String device, String nodeId, String channelId) { String key = "im:conn:" + userId; String value = nodeId + "|" + channelId; redisTemplate.opsForHash().put(key, device, value); redisTemplate.opsForHash().put(key, device + ":activeAt", String.valueOf(System.currentTimeMillis())); redisTemplate.expire(key, Duration.ofMinutes(10)); } public Map<Object, Object> routes(Long userId) { return redisTemplate.opsForHash().entries("im:conn:" + userId); } public void heartbeat(Long userId) { redisTemplate.expire("im:conn:" + userId, Duration.ofMinutes(10)); } }

这个结构的关键点不是代码本身,而是数据维度。

它不是只记录用户在线,而是记录用户在哪些设备在线、每个设备在哪个节点、对应哪个连接。

后续消息投递、多端同步、跨节点转发,都依赖这张路由表。

一条消息真正经历了哪些阶段

一条消息从客户端发出后,不应该直接写成“保存并推送”。更稳妥的方式是把它拆成状态流转。

客户端本地生成 clientMsgId ↓ 进入本地发送队列 ↓ 通过 WebSocket / Socket 发送 ↓ 服务端解析协议 ↓ 校验用户与会话状态 ↓ 判断 clientMsgId 是否重复 ↓ 生成 serverMsgId 和 msgSeq ↓ 写入消息存储 ↓ 返回 ACK 给发送端 ↓ 根据在线路由投递到接收端 ↓ 接收端确认或等待后续同步

这里面有三个 ID 很重要。

clientMsgId用于解决客户端弱网重发。客户端网络不稳定时,同一条消息可能被发送多次,服务端需要根据它做幂等判断。

serverMsgId是服务端消息唯一 ID。消息撤回、引用回复、收藏、合并转发、历史查询都会用到它。

msgSeq是会话内递增序号。它用于解决消息排序、分页查询、多端同步和断点续拉问题。

如果只依赖时间戳排序,在并发发送、跨节点投递或客户端本地临时消息存在时,容易出现顺序不稳定。

消息协议不要围绕页面设计

聊天页面上看到的是文本、图片、文件、表情、红包、名片、语音、视频等不同形态,但服务端不应该为每种形态设计一套协议。

更合理的协议是命令 + 消息体 + 扩展字段。

{ "cmd": "MSG_SEND", "clientMsgId": "c_10001_202607010001", "serverMsgId": null, "conversationId": "c2c_10001_20001", "fromUserId": 10001, "targetId": 20001, "scene": "single", "bodyType": "text", "body": { "text": "这是一条即时消息" }, "extra": { "quoteMsgId": null, "atUserIds": [], "forwardId": null }, "sendAt": 1782864000000 }

这个结构里,cmd描述操作,bodyType描述内容类型,extra描述消息之间的关系。

例如消息撤回不是一种正文内容,而是一种命令。

已读回执不是聊天气泡,而是状态事件。

音视频邀请也不是普通文本,而是信令消息。

这样设计后,消息类型增加时,协议主结构不需要频繁变化。服务端只需要根据cmdbodyType分发到不同处理器。

写入消息表前先确定查询方式

IM 消息表的设计不能只看“要存哪些字段”,还要先看“未来怎么查”。

聊天窗口向上翻页,需要按会话查询历史消息。

客户端断线重连,需要按上次同步位置补消息。

引用回复,需要按消息 ID 查原消息摘要。

消息撤回,需要按消息 ID 更新状态。

合并转发,需要按一组消息 ID 查询上下文。

因此,消息存储至少要支持三种查询路径:

conversationId + msgSeq 用于会话分页 serverMsgId 用于单条消息定位 senderId + clientMsgId 用于发送幂等

表结构可以围绕这些路径设计:

CREATE TABLE im_message_store ( id BIGINT PRIMARY KEY AUTO_INCREMENT, server_msg_id VARCHAR(64) NOT NULL, client_msg_id VARCHAR(64) DEFAULT NULL, conversation_id VARCHAR(64) NOT NULL, sender_id BIGINT NOT NULL, msg_seq BIGINT NOT NULL, scene VARCHAR(20) NOT NULL, body_type VARCHAR(30) NOT NULL, body JSON NOT NULL, msg_status TINYINT DEFAULT 0, send_at BIGINT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_server_msg_id (server_msg_id), UNIQUE KEY uk_conversation_seq (conversation_id, msg_seq), KEY idx_sender_client (sender_id, client_msg_id), KEY idx_conversation_time (conversation_id, send_at) );

uk_conversation_seq可以保证同一个会话内消息序号不重复。

idx_sender_client可以防止客户端重发造成重复消息。

msg_status用于表达正常、已撤回、已删除、异常等状态。撤回消息时,通常不物理删除,而是更新状态并推送撤回事件。

当消息量变大后,还需要考虑冷热数据拆分。近期消息用于高频查询,历史消息可以进入归档表或独立存储,避免单表持续膨胀影响分页性能。

ACK 不是提示,而是可靠性边界

很多聊天系统的问题都出在 ACK 设计不清楚。

客户端点击发送后,可以先把消息展示在本地,但此时只能算“发送中”。只有服务端完成协议解析、幂等判断、消息编号和落库后,返回 ACK,客户端才能把状态改成“已发送”。

如果服务端没有返回 ACK,客户端需要决定是重试、标记失败,还是等待网络恢复。

这也是clientMsgId必须存在的原因。重试时服务端看到相同clientMsgId,应该返回同一条消息的处理结果,而不是重新生成一条新消息。

ACK 的意义不是“告诉用户发出去了”,而是划定客户端状态和服务端状态的分界线。

群聊高并发的核心不是消息内容,而是扩散策略

群聊最容易出性能问题的地方,不是消息表存一条内容,而是这条消息要影响多少用户。

一个群有 500 个成员时,一条消息可能带来:

500 个成员的未读状态变化。

若干在线成员的实时推送。

若干离线成员的后续同步。

被 @ 成员的特殊提醒。

多端在线用户的状态刷新。

如果每发一条群消息都给每个成员复制一份完整消息,写入量会快速放大。

更常见的方式是:

消息正文只存一份。

会话内生成递增msgSeq

每个成员维护自己的lastReadSeq

未读数可以由读取位置计算,也可以在 Redis 中做高频计数。

在线成员实时推送,离线成员通过同步机制补齐。

这种设计可以把“消息内容存储”和“成员读取状态”拆开,避免群成员数量直接放大消息表写入压力。

已读未读本质是读取位置,不是布尔值

在 IM 系统中,已读未读不能简单设计成read = true / false

对于单聊,用户只需要知道对方读到哪里。

对于群聊,每个成员都可能读到不同位置。

因此,已读状态更适合用读取位置表达:

conversationId = group_90001 userId = 10001 lastReadSeq = 3688 lastReadAt = 1782867600000

当会话最大消息序号是 3700,而用户最后已读序号是 3688,就可以计算出未读区间。

这种方式也更适合多端同步。用户在 PC 端打开会话后,服务端更新lastReadSeq,然后向 Android、iOS、H5 推送已读事件,各端再更新本地未读数。

如果未读数只在客户端本地维护,换设备、断线重连、清理缓存后很容易出现不一致。

多端同步不要伪装成普通消息

Android、iOS、H5、PC 同时在线时,长连接里传输的不只有聊天消息,还有大量状态事件。

例如:

PC 端已读,会影响移动端未读数。

App 端撤回消息,会影响 H5 和 PC 的消息展示。

用户修改资料,会影响会话列表展示。

群信息变更,会影响所有在线成员的群资料缓存。

这些数据不应该都伪装成普通聊天气泡,而应该以事件方式处理。

可以把长连接下发数据分成两类:

聊天消息:需要进入消息列表 状态事件:只更新本地状态或页面缓存

例如MSG_REVOKE是状态事件,客户端收到后查找本地对应消息并更新为已撤回。

READ_RECEIPT也是状态事件,客户端收到后更新未读数或消息已读状态。

GROUP_PROFILE_UPDATE不进入聊天记录,只刷新群资料缓存。

这样客户端处理逻辑会更清晰,也能减少大量无意义的系统气泡。

文件和音视频都不应该压在消息通道里

实时消息通道适合传输轻量数据,不适合传输大文件和音视频媒体流。

文件发送更合理的方式是先走 HTTP 上传,拿到文件元数据后,再发送一条文件类型消息。消息里保存文件 ID、文件名、大小、类型、下载地址等信息。

语音通话、视频通话、多人音视频会议也类似。IM 负责发送信令,例如邀请、接听、拒绝、挂断、加入会议、离开会议。真正的媒体流应该交给 RTC 通道处理。

这种拆分有两个好处。

第一,长连接不会被大文件或媒体流阻塞。

第二,文件存储、下载鉴权、音视频房间、媒体传输都可以独立扩展,不会和聊天消息强耦合。

跨节点投递要依赖路由,而不是广播

单机部署时,用户连接都在当前进程内,服务端可以直接找到连接并推送。

集群部署后,情况会变成:

用户 A 连接在 im-node-01 用户 B 连接在 im-node-03 用户 C 连接在 im-node-02

用户 A 给用户 B 发消息时,im-node-01不能盲目广播给所有节点,而应该先查 Redis 路由,确认用户 B 在im-node-03,然后把消息转发到目标节点。

跨节点转发可以通过内部 RPC、消息队列或 Redis Pub/Sub 实现。无论采用哪种方式,都要保证两点:

同一条消息不会重复投递。

目标节点异常时,消息仍然可以通过历史同步兜底。

这里的关键仍然是serverMsgId和幂等处理。只要每条消息有稳定的全局 ID,接收节点就可以判断自己是否已经处理过。

朋友圈、红包、钱包这类数据不要挤进消息主表

IM 系统经常会扩展社交和交易相关能力,但技术上不应该把所有业务都放进消息表。

朋友圈更接近内容流,核心是动态、评论、点赞、可见范围。

红包和钱包更接近资金状态,核心是余额、冻结、领取记录、流水。

聊天消息可以展示一个入口或状态摘要,但数据主状态应该由独立模型维护。

例如红包消息里可以保存红包 ID 和展示摘要,但领取状态、金额变化、退款状态、钱包流水不能依赖聊天消息表。

这样做的原因很简单:消息表主要服务会话查询,高频读写已经足够复杂。如果把内容流、资金流水、评论点赞都塞进去,索引会变得混乱,后续归档和排查也会变困难。

i18n 多语言也要走统一事件和 key

四端系统很容易出现文案不一致的问题。Android、iOS、H5、PC 如果各自维护一套语言 key,后期同一个功能可能出现不同翻译。

更稳定的方式是统一 key 命名。

例如:

chat.send chat.revoke message.unread group.notice wallet.balance file.upload call.invite meeting.join

前端根据语言环境加载不同语言包。后端发送系统通知时,也可以根据用户语言偏好选择对应文案。

i18n 看起来是前端问题,但在 IM 系统中,系统通知、群事件、钱包提醒、会议邀请都可能由后端生成,所以语言 key 最好从全局统一设计。

排查 IM 问题要顺着链路走

IM 系统出问题时,不能只看某一个接口是否返回成功。

如果用户反馈“消息发出去了,对方没收到”,可以按链路排查:

客户端是否生成了clientMsgId

服务端是否返回 ACK。

消息是否写入消息表。

接收方 Redis 路由是否存在。

目标连接是否还有效。

跨节点转发是否成功。

接收端是否做了消息去重。

如果用户反馈“未读数不准”,可以检查:

会话最大msgSeq是否正确。

用户lastReadSeq是否更新。

多端已读事件是否下发。

Redis 未读计数是否重复累加。

客户端本地状态是否覆盖服务端状态。

如果用户反馈“群消息顺序错乱”,可以检查:

同一会话内msgSeq是否连续。

客户端是否按服务端序号排序。

本地临时消息是否正确替换为服务端消息。

跨节点投递是否导致重复推送。

如果用户反馈“PC 有消息,手机没有消息”,可以检查:

Redis 是否按设备维度保存路由。

移动端连接是否被后登录设备覆盖。

服务端是否只推送到单端。

移动端断线重连后是否主动补拉消息。

这类排查方式比单纯查看 Controller 日志更有效。IM 系统的稳定性来自连接、协议、消息编号、存储索引、Redis 路由、状态事件和客户端本地队列之间的协作。任何一个环节状态不一致,最终都会表现为延迟、重复、丢失、顺序异常或多端不同步。

技术实现思路简要归纳宠友(IM即时通讯)app,支持语音、文件、图片、视频等多种类型消息的发送,安全可靠,交流轻松,私有化部署,快速开发,极简部署,支持群聊管理、语音视频通话...功能https://chongyou.info/1/product/im.html

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/2 10:55:49

打卡信奥刷题(3421)用C++实现信奥题 P10178 陌路寻诗礼

P10178 陌路寻诗礼 题目背景作为 luogu 网红的帆巨&#xff0c;有非常多狂热的粉丝&#xff0c;而我们的帆巨也很喜欢面基&#xff0c;寻找遍布大江南北的粉丝们。 题目描述 帆巨所在的家乡的地图是一张有 nnn 个节点 mmm 条有向道路的有向图&#xff0c;每个节点都是一个城市&…

作者头像 李华
网站建设 2026/7/2 10:53:39

分享我的开源项目: 基于Go开发的微服务即时通讯与社交平台

工作之余断断续续开发了一年时间&#xff0c;欢迎stars go-hichat-api English | 简体中文 go-hichat-api 是Go语言后端与 Web 客户端仓库&#xff0c;是一个基于 go-zero 的微服务即时通讯与社交平台。项目整合 REST API、zRPC 服务、WebSocket 长连接、Kafka 异步链路、Mon…

作者头像 李华
网站建设 2026/7/2 10:53:35

ASM330LHH与STM32F410RB运动跟踪系统设计指南

1. ASM330LHH与STM32F410RB的硬件组合解析1.1 ASM330LHH的6DoF IMU特性拆解ASM330LHH这颗汽车级6轴惯性模块采用系统级封装(SiP)技术&#xff0c;在3.3mm2.6mm0.83mm的微型封装内集成了三轴数字加速度计和三轴数字陀螺仪。实测中&#xff0c;其加速度计量程可配置为2/4/8/16g&a…

作者头像 李华
网站建设 2026/7/2 10:48:54

嵌入式条码扫描系统开发:LV30与PIC18F85J10实战解析

1. 项目背景与硬件选型解析在工业自动化和零售管理领域&#xff0c;条码扫描技术已经渗透到各个环节。我最近完成了一个嵌入式条码扫描系统的开发项目&#xff0c;核心目标是实现多介质环境下的高兼容性条码识别。这个方案采用了LV30扫描头与PIC18F85J10微控制器的组合&#xf…

作者头像 李华
网站建设 2026/7/2 10:47:17

AD74412R与MKV46F256VLH16工业级信号处理方案解析

1. AD74412R与MKV46F256VLH16的黄金组合&#xff1a;工业级性能提升方案在工业自动化和过程控制领域&#xff0c;信号采集与处理的实时性、精度要求越来越高。ADI的AD74412R四通道可配置I/O芯片与NXP的MKV46F256VLH16 ARM Cortex-M4微控制器的组合&#xff0c;恰好能满足这一需…

作者头像 李华