即时通讯源码从消息入口到异步任务的后端分层

即时通讯源码从消息入口到异步任务的后端分层
即时通讯源码如果只围绕“发送消息接口”组织代码后期会很容易变重。一开始只是文本、图片、文件、表情继续往后已读回执、消息撤回、收藏、朋友圈互动、红包状态、钱包流水、群成员变更、语音视频邀请、多人会议加入离开都会进入同一条实时链路。这些行为不一定都应该写进聊天消息表也不一定都应该被客户端渲染成聊天气泡。更合适的方式是把它们统一抽象成事件再按事件类型进入不同处理链路。从事件如何进入系统、如何分发、如何存储、如何异步处理、如何同步到多端来看 SpringBoot WebSocket Redis Stream 的设计方式。客户端行为 ↓ WebSocket 接入层 ↓ 事件包装 EventEnvelope ↓ 同步主链路 ├─ 权限校验 ├─ 状态校验 ├─ 核心数据落库 └─ ACK 返回 ↓ Redis Stream 异步事件 ├─ 会话刷新 ├─ 通知提醒 ├─ 事件日志 ├─ 搜索索引 └─ 多端状态同步这张链路草图的重点是实时操作和异步任务不要挤在同一个方法里。用户发送一条文件消息核心链路只需要处理文件引用、消息落库、ACK 返回和在线推送。至于搜索索引、操作日志、通知提醒、资源引用统计可以交给异步事件处理。用户撤回一条消息核心链路只需要校验权限、更新消息状态、推送撤回事件。至于多端缓存刷新、审计日志、会话摘要更新可以作为后续事件消费。这样可以避免 WebSocket 入口越来越厚。先统一事件外壳再处理业务差异即时通讯系统里的事件来源很多。用户发文本是事件。用户发文件也是事件。用户收藏消息是事件。用户读完会话是事件。用户发起视频通话是事件。用户进入多人会议也是事件。如果每种行为都独立写一套入参结构后端很快就会堆出大量相似代码。更稳定的方式是先定义统一事件外壳再把业务差异放进 payload。public class ImEventEnvelope { private String eventId; private String traceId; private String eventType; private Long userId; private String device; private String conversationId; private Long createdAt; private MapString, Object payload; public boolean isMessageEvent() { return eventType ! null eventType.startsWith(MESSAGE_); } public boolean isStateEvent() { return eventType ! null eventType.startsWith(STATE_); } public boolean isSignalEvent() { return eventType ! null eventType.startsWith(SIGNAL_); } }这个结构可以覆盖多类场景MESSAGE_SEND表示普通消息发送文本、图片、文件、表情、引用回复、合并转发都可以走这类事件。STATE_READ表示已读回执不需要进入聊天气泡只需要更新读取位置和未读数。STATE_REVOKE表示消息撤回客户端收到后更新本地消息状态。SIGNAL_CALL_INVITE表示语音或视频邀请IM 通道只传信令不传媒体流。SIGNAL_MEETING_JOIN表示加入多人会议可以同步会议成员状态。这种设计可以让聊天消息、状态同步、音视频信令分开处理而不是全部塞进一个“消息发送”方法里。事件类型不要直接等于页面功能页面上看到的入口不一定等于后端事件类型。比如“发送文件”这个动作至少可以拆成两个事件RESOURCE_UPLOADED MESSAGE_SEND文件先通过 HTTP 上传生成资源 ID随后 WebSocket 发送一条文件消息消息里只引用 resourceId。“收藏文件”也不是复制文件内容而是产生一个收藏状态事件STATE_FAVORITE_ADD收藏表保存对象类型和对象 ID。对象可以是 messageId、resourceId、postId 或 forwardId。“朋友圈评论提醒”也不应该直接写进聊天消息表。朋友圈本身属于内容流评论产生后可以转成通知事件CONTENT_COMMENT_CREATED STATE_NOTIFY_PUSH这样聊天系统仍然保持会话结构朋友圈保持内容流结构两者通过事件发生关联。Redis Stream 承接非核心任务SpringBoot 主链路处理完核心数据后可以把后续任务写入 Redis Stream。这样不会让消息发送、撤回、已读这类实时操作被日志、索引、统计任务拖慢。Service public class ImEventPublisher { private final StringRedisTemplate redisTemplate; public ImEventPublisher(StringRedisTemplate redisTemplate) { this.redisTemplate redisTemplate; } public void publish(ImEventEnvelope event) { MapString, String body new HashMap(); body.put(eventId, event.getEventId()); body.put(traceId, event.getTraceId()); body.put(eventType, event.getEventType()); body.put(userId, String.valueOf(event.getUserId())); body.put(device, event.getDevice()); body.put(conversationId, event.getConversationId()); body.put(payload, JsonUtils.toJson(event.getPayload())); redisTemplate.opsForStream().add(im:event:stream, body); } }可以进入 Redis Stream 的任务包括消息发送后的会话摘要刷新。消息撤回后的多端状态同步。文件消息后的资源引用统计。收藏变更后的收藏列表刷新。朋友圈互动后的通知生成。红包状态变化后的事件日志记录。多人会议状态变化后的在线端通知。这里的关键点不是“所有事件都异步”而是区分主链路和扩展链路。主链路要保证实时反馈。异步链路负责后续扩散。事件日志不是消息表的替代品即时通讯源码里常见一个误区已经有消息表了就不需要事件日志。实际上两者职责不同。消息表记录会话中的聊天内容。事件日志记录系统中发生过的关键动作。用户发了一条文本消息消息表需要保存正文事件日志可以记录这次发送操作的 traceId、设备、处理状态。用户撤回消息消息表更新状态事件日志记录是谁撤回、从哪个端撤回、影响了哪条消息。用户加入群聊群成员表写入关系事件日志记录入群来源是扫码、邀请还是后台加入。用户领取红包钱包流水记录金额变化事件日志记录该业务动作和会话事件之间的关联。事件日志可以这样设计CREATE TABLE im_event_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, event_id VARCHAR(64) NOT NULL, trace_id VARCHAR(64) NOT NULL, event_type VARCHAR(64) NOT NULL, user_id BIGINT NOT NULL, device VARCHAR(32) DEFAULT NULL, conversation_id VARCHAR(64) DEFAULT NULL, target_id VARCHAR(64) DEFAULT NULL, event_status TINYINT DEFAULT 0, payload JSON, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_event_id (event_id), KEY idx_trace_id (trace_id), KEY idx_user_time (user_id, created_time), KEY idx_conversation_time (conversation_id, created_time) );有了事件日志排查问题时可以按 traceId 找到一次操作的完整路径。比如用户反馈“收藏后 PC 端没刷新”可以查看收藏事件是否生成。事件是否写入 Redis Stream。多端推送是否执行。PC 端是否在线。客户端是否消费了状态事件。如果没有事件日志只能分别翻消息表、收藏表、WebSocket 日志和客户端日志定位成本会高很多。聊天消息和状态事件分流即时通讯系统里的 WebSocket 下发数据不应该全部进入聊天列表。可以分成三类第一类聊天类事件进入消息列表例如文本、图片、文件、语音、视频、表情、名片、引用回复、合并转发摘要。第二类状态类事件不进入消息列表只更新状态例如已读回执、消息撤回、会话置顶、免打扰、收藏变更、群资料变更。第三类信令类事件进入实时通话或会议流程例如语音邀请、视频邀请、会议创建、成员加入、成员离开、挂断。客户端收到事件后应该先判断 eventType再决定更新哪个本地模块。MESSAGE_* - 聊天窗口 STATE_* - 会话状态或本地缓存 SIGNAL_* - 通话与会议流程 CONTENT_* - 朋友圈或通知入口这种分流可以避免一个问题所有后端事件都被当成聊天气泡。如果撤回、已读、收藏、群资料更新、会议状态都以普通消息方式渲染聊天列表会越来越混乱客户端逻辑也会越来越难维护。一次文件收藏如何穿过事件系统以“用户收藏一条群聊文件消息”为例后端可以按事件链处理MESSAGE_SEND 文件消息进入会话 消息体引用 resourceId STATE_FAVORITE_ADD 收藏 messageId 或 resourceId RESOURCE_REF_UPDATED 文件资源引用计数变化 STATE_SYNC_PUSH 推送收藏变更到其他在线端 EVENT_LOG_CREATED 记录本次收藏操作这里不需要复制文件内容也不需要把收藏数据写回消息表。消息表负责聊天记录。资源表负责文件元数据。收藏表负责对象引用。事件流负责状态同步。这样后期即使文件消息被删除系统也能根据引用关系判断资源是否仍被收藏使用。一次多人会议邀请如何穿过事件系统多人会议也可以按事件处理而不是当成普通文本消息。SIGNAL_MEETING_CREATE 创建会议房间 SIGNAL_MEETING_INVITE 邀请成员加入会议 STATE_NOTIFY_PUSH 推送到在线端 SIGNAL_MEETING_JOIN 成员加入会议 SIGNAL_MEETING_LEAVE 成员离开会议IM 通道只处理信令媒体流不进入 WebSocket 消息链路。这样语音通话、视频通话、多人会议都可以复用信令事件模型。聊天系统不需要承载音视频流只需要维护呼叫状态、房间状态和成员状态。红包状态不要直接塞进聊天消息红包在聊天窗口中可以展示为一种卡片但红包金额和钱包流水不应该依赖消息表。事件链可以这样拆MESSAGE_SEND 会话中展示红包卡片 WALLET_RED_PACKET_CREATED 创建红包单据 WALLET_RED_PACKET_RECEIVED 用户领取红包 WALLET_LEDGER_CREATED 写入钱包流水 STATE_RED_PACKET_SYNC 同步红包状态到多端聊天消息只保存红包 ID 和展示摘要。红包模块保存金额、数量、剩余状态。钱包模块保存余额变化和流水。事件系统负责把这些动作串起来但不让资金数据污染消息主表。事件驱动架构需要关注这些细节事件驱动不是把所有逻辑都异步化也不是把所有业务都塞进队列。即时通讯源码里使用事件模型时需要注意几个工程细节事件 ID 必须唯一 traceId 必须贯穿同步和异步链路 事件处理器必须幂等 核心链路不能被异步任务阻塞 状态事件不能全部渲染成聊天消息 Redis Stream 消费失败需要重试策略 事件日志要能按 userId 和 conversationId 查询 消息表不要承担文件、钱包、朋友圈的全部状态 客户端要按事件类型分流处理这里最容易忽略的是幂等。Redis Stream 中的事件可能因为重试被消费多次。如果处理器没有幂等控制就可能出现重复刷新、重复通知、重复写日志甚至重复产生业务结果。因此事件处理器需要根据 eventId 或业务唯一键判断是否已经处理过。目录结构也可以按事件边界组织即时通讯源码可以不按页面目录组织而按事件边界组织。im-gateway WebSocket 接入、协议解析、连接管理 im-event-core 事件模型、事件分发、事件日志 im-message 聊天消息、会话状态、已读回执 im-resource 文件、图片、语音、视频资源 im-wallet 红包、钱包、资金流水 im-content 朋友圈、评论、点赞 im-notify 多端推送、系统通知、状态事件 im-signal 语音视频、多人会议信令这样新增能力时代码落点会更清楚。新增一种消息类型进入 im-message。新增文件收藏关联 im-resource 和 im-notify。新增红包领取进入 im-wallet。新增朋友圈评论提醒进入 im-content 和 im-notify。新增多人会议邀请进入 im-signal。不同业务通过事件连接而不是互相直接调用一堆内部方法。事件模型让即时通讯源码从接口堆叠变成链路编排即时通讯系统不是普通 CRUD 系统。它的复杂度来自实时连接、会话状态、多端同步和业务扩展之间的叠加。如果所有能力都写进 WebSocket 消息入口代码会越来越重。如果所有状态都写进消息表数据会越来越混乱。如果所有下发都当成聊天消息客户端渲染会越来越复杂。事件驱动架构的价值是把不同类型的变化分开聊天消息走消息链路。已读撤回走状态链路。文件收藏走资源链路。红包钱包走资金链路。朋友圈互动走内容链路。语音视频和多人会议走信令链路。Redis Stream 承接异步扩散MySQL 记录关键状态和事件日志WebSocket 只负责实时事件下发客户端按事件类型更新对应页面。对于 SpringBoot 即时通讯源码来说这种结构能让单聊、群聊、文件、收藏、朋友圈、红包、语音视频、多人会议这些能力自然接入而不是不断堆到同一个消息发送方法里。相关概念的资料整理笔记宠友(IM即时通讯)app,支持语音、文件、图片、视频等多种类型消息的发送,安全可靠,交流轻松,私有化部署,快速开发极简部署支持群聊管理、语音视频通话...功能https://chongyou.info/1/product/im.html