文章目录
- [IM 顶层设计解析](#IM 顶层设计解析)
-
- [1. 完善IM系统需要考虑的方面](#1. 完善IM系统需要考虑的方面)
- [2. 消息架构组件定义](#2. 消息架构组件定义)
- [3. 消息交互大致流程 (推拉结合设计)](#3. 消息交互大致流程 (推拉结合设计))
- [4. 集群推送方案设计](#4. 集群推送方案设计)
-
- [4.1 WebSocket 与 HTTP 区别](#4.1 WebSocket 与 HTTP 区别)
- [4.2 单机连接管理(失效方案)](#4.2 单机连接管理(失效方案))
- [4.3 方案一:Redis 存储 Channel (不可行)](#4.3 方案一:Redis 存储 Channel (不可行))
- [4.4 方案二:精准投递消息 (Precise Message Delivery)](#4.4 方案二:精准投递消息 (Precise Message Delivery))
- [4.5 方案三:分层路由 (解决连接爆炸)](#4.5 方案三:分层路由 (解决连接爆炸))
- [5. 消息时序性与唯一性](#5. 消息时序性与唯一性)
- [6. 推送策略总结](#6. 推送策略总结)
- [7. 百万直播间推送方案 (广播推送)](#7. 百万直播间推送方案 (广播推送))
- [8. 消息可靠性保证](#8. 消息可靠性保证)
-
- [8.1 消息发送的可靠性 (客户端 -> 服务端)](#8.1 消息发送的可靠性 (客户端 -> 服务端))
- [8.2 消息推送的可靠性 (服务端 -> 客户端)](#8.2 消息推送的可靠性 (服务端 -> 客户端))
- 表结构设计
-
- 核心表关系
-
- 关系图结构化说明
- [1. 房间表(room)](#1. 房间表(room))
- [2. 单聊扩展表(single_chat)](#2. 单聊扩展表(single_chat))
- [3. 群聊扩展表(group_chat)](#3. 群聊扩展表(group_chat))
- [4. 群成员表(group_member)](#4. 群成员表(group_member))
- [5. 消息表(message)](#5. 消息表(message))
- [6. 会话表(contact,即优化后的用户收信箱)](#6. 会话表(contact,即优化后的用户收信箱))
- [7. 热点群聊收信箱(hot_room_inbox,可选,独立存储热点群聊会话)](#7. 热点群聊收信箱(hot_room_inbox,可选,独立存储热点群聊会话))
- 核心设计说明
IM 顶层设计解析
1. 完善IM系统需要考虑的方面
在设计一个完善的IM系统时,需要考虑以下关键方面:

以下是转换后的 Markdown 格式(按原结构整理,保持层级清晰):
- 集群消息路由
- 用户不在同一个websocket服务上,怎么通信
- 点对点/广播
- 消息时序性
- 跟随时间戳/d
- 全局时序
- 会话时序
- 设备端时序
- 消息id生成
- 唯一保证
- 顺序保证
- 单调递增
- 趋势递增
- 消息可靠ACK
- 发送可靠/推送可靠
- 在线推送可靠/离线推送可靠
- 消息重复
- 分布式特性:可靠就会重复,重试就会重复,重发就会重复等
- 前端重复发
- 后端重复推
- 推拉结合
- 推拉结合,平衡时效性和可靠性
- 多端同步
- 单聊群聊
- 怎么兼容单聊和群聊的消息存储和推送
- 消息已读未读
- 已读未读,同时关联到用户读消息的ack设计,如果是全量写,每一条消息又是指数上升
- 热点扩散风暴
- 全员消息一条,一条消息写入所有成员的信箱,就是造成的扩散系数增长
- 表结构设计
- 多类型消息表结构设计
- 用户收信箱表结构设计
- 已读未读表结构设计
- 单聊群聊表结构设计
要不要我帮你整理一份IM系统设计各模块的核心方案总结?方便你快速了解每个模块的解决思路。
2. 消息架构组件定义
以下是 MallChat 架构中考虑的几个关键服务组件及其职责:
- WebSocket 服务 (有状态):维护和用户的连接通道,可以接收消息,也可以推送消息。
- IM 服务 (无状态):负责消息的发送逻辑,处理单聊/群聊的消息。
- Logic 服务:处理用户的心跳、上下线、联系人、加好友、创群组等逻辑。
- Auth 服务:处理用户认证、权限等需求。
- Router:推送消息时,负责确保消息被正确、可靠地推送到用户所在的 WebSocket 服务上。

3. 消息交互大致流程 (推拉结合设计)
以下是一个群消息发送到推送的流程:
- 连接建立:用户A和 WebSocket 服务建立连接。之后都通过该连接发送消息,接受消息。
- 消息发送:用户A发送群消息,WebSocket 服务将消息通过 Dubbo 转发给 IM 服务 (无状态,通过负载均衡随机发送)。
- 持久化与投递 :IM 服务将消息持久化,然后将消息投递到 消息队列 MQ。MQ 快速响应前端,消费者根据负载进行后续的推送、写扩散等操作。
- 写扩散/热点信箱 :
- 热点群聊 :只写入 热点信箱。
- 单聊/普通群聊:写扩散到每个群成员信箱 (如写入B和C的信箱)。
- 消息推送 :将消息投递信箱后,进行推送。
- 在线:进行 WebSocket 推送。
- 离线:进行 push 通知。
- 路由 :由于用户连接在不同的 WebSocket 上,需要 Router 服务 推送到 B 和 C 所在的不同 WebSocket。
- 可靠性保证:推送时需确保消息可靠性,可能需要做应用层的 ACK,类似 TCP 的滑动窗口确认。
- 会话查询 :用户查询会话列表时,需要一个 聚合层 聚合用户信箱和热点信箱,并严格排序后返回给用户(即 推拉结合)。
4. 集群推送方案设计
4.1 WebSocket 与 HTTP 区别
| 特性 | HTTP | WebSocket |
|---|---|---|
| 状态 | 无状态 | 有状态 |
| 连接 | 每次请求重新握手建立 TCP 连接 | 建立连接后,一直复用该连接 |
| 负载均衡 | 可随意进行负载均衡 | 收发信息在同一个机器,重连后才更换连接 |
4.2 单机连接管理(失效方案)
MallChat 使用 Netty 实现 WebSocket,连接即为一个 Channel。在单机环境下,登录时将 UID 和 Channel 关联缓存在 JVM 的 Map 中,推送时通过 UID 取出 Channel 进行推送。
问题:在集群场景下,要推送的用户连接在别的机器上,IM服务不知道对应的 WebSocket 机器在哪。连接管理在 JVM 层面,导致 Router 无法定位。
4.3 方案一:Redis 存储 Channel (不可行)
设想 :使用中心化的 Redis 存储 UID 和 Channel 的映射关系。
问题 :Channel 是本地的 Socket 连接,无法进行存储和反序列化。
4.4 方案二:精准投递消息 (Precise Message Delivery)
核心思路 :本地依然维护 UID 和 Channel 的关系,而 Redis 维护用户的连接状态(即用户在哪台机器上连接,如 IP)。
流程描述:
- 消息发送 :A 通过其连接的
channel发送消息给WebSocket,该 WebSocket 通过 Dubbo 转发给 IM 服务。 - 消息持久化 :IM 服务持久化消息后,调用 Router,推送消息给 C。
- 路由定位 :
Router通过 Redis 查到 C 目前连接的 WebSocket IP,然后通过 TCP 连接对指定的 WebSocket 服务器进行消息推送。 - 本地投递 :目标 WebSocket 服务收到请求后,通过本地连接管理查出用户具体的
channel,然后进行推送。
方案问题:
- Redis 更新频繁:需要频繁更新 Redis 维护用户和 WebSocket 服务的映射 (可复用上下线功能解决)。
- 连接数爆炸 :Router 必须连接所有的 WebSocket。如果用户体量大,WebSocket 需要水平扩容,Router 数量也会增加,导致 Router 与 WebSocket 之间的连接数爆炸(达到上千连接),占用 WebSocket 资源。
4.5 方案三:分层路由 (解决连接爆炸)
核心思路:将 Router 与 WebSocket 的连接分组管理,避免 Router 连接所有 WebSocket,在中间增加一层路由,并设定路由规则。
优缺点:
- 优点:有效减少连接数。
- 缺点:增加消息的推送链路。
- 适用场景 :适用于真的很大型的集群场景。

5. 消息时序性与唯一性
5.1 消息ID的重要性
消息ID需满足 唯一 和 有序(递增) 两个需求。
5.2 有序性保证
| 有序性类型 | 描述 | 实现挑战 |
|---|---|---|
| 全局递增 | 消息在整个IM系统都是唯一且递增。 | 分库分表后,分布式ID通常保证 趋势递增 ,而非 单调递增。实现单调递增存在单点竞争问题。 |
| 会话递增 | 保证同一个会话内的消息递增。 | - |
| 收信箱递增 | 适用于写扩散场景,每个人都有自己的收信箱,维护自己的时间线。 | - |
方案:
- MallChat 方案:MallChat 使用表ID自增。
- 行业方案:微信是典型的写扩散场景,群聊人数上限为 500 人,其序列号生成器方案可作为参考。
https://cloud.tencent.com/document/product/269/2282
-
单聊消息
MsgSeq字段的作用及说明:该字段在发送消息时由用户自行指定,该值可以重复,非后台生成,非全局唯一。单聊消息历史记录对同一个会话的消息先以时间戳排序,同秒内的消息再以MsgSeq排序。 -
与群聊消息的 MsgSeq 字段不同,群聊消息的 MsgSeq 由后台生成,每个群都维护一个 MsgSeq,从1开始严格递增。
总结一下消息顺序性的实现:目前最有效的方法就是,维护单独的消息序号,然后对于维护的序号,他一定要做到全局单调递增。这是很困难的,所以我们可以退而求其次,模仿微信的方式,做到群聊表内的单点递增,将消息id存在Redis中,做到唯一递增。
这里可能会遇到Redis突然挂了,所以要设计一个备用流程,提前预估好短时间内会进来多少条消息,例如1000条,在Redis挂的时候,本地对这些消息,从最新的消息id开始进行递增,直到Redis重新上线。
客户端排序
为了解决用户快速发两三条消息时间内的局部排序而已。可以参考腾讯sdk的实现。
给消息设置一个本地的自增id,发送消息的时候带上。排序整体以服务器的时间为准, 相同秒内的排序以自增id为准。

腾讯IM方案
单聊消息 MsgSeq字段的作用及说明:该字段在发送消息时由用户自行指定,该值可以重复,非后台生成,非全局唯一。
与群聊消息的MsgSeq字段不同,群聊消息的MsgSeq由后台生成,每个群都维护一个MsgSeq,从1开始严格递增。单聊消息历史记录对同一个会话的消息先以时间戳排序,同秒内的消息再以MsgSeq排序。
这样赌的就是,你发的消息再快,哪怕存储顺序变了,但是也都在1s内,对于b来说,1s内的消息,按照自增id额外排序就好了。
这样发送者只需要保证指定时间内的消息自增就好。如果哪天seq丢失,或者在其他端发消息seq不一致,都没关系。
但是在群聊的场景下,每个群成员的客户端时间也是不一样的,没法作为排序的统一基准时间。只能采用服务端时间。并且seq也是不同的,相同秒内也没法排序,不适用该方案
服务端排序
对于群聊的场景,我们需要采用服务端排序。服务端排序其实本应该很简单。
对于单表来说,我们可以采用主键id来排序, 也可以通过消息的时间戳来排序。id肯定是严格自增了,时间戳要考虑精度问题, 一般设置的精度是毫秒, 也基本足够进行消息的排序了。因为毫秒内的消息,本身就没有上下文的关系, 对顺序要求不高。
除了消息的顺序性 外,还有一个很重要的点,就是消息的唯一性 。在游标翻页的场景下。翻页的游标字段,需要同时保证顺序性与唯一性的作用。如果单纯用时间戳,毫秒内的消息,就没办法区分与排序,得额外再拼接其他唯一字段一同排序,不如就直接用唯一且有序的id作为游标。

因此消息的时序性,我们通常都是用一个唯一id来保证。微信sdk也是用一个id来进行翻页。
6. 推送策略总结
- 单聊消息多 :用 精准投递。
- 群聊消息多 :用 集群广播推送。
- MallChat 倾向 :MallChat 倾向于使用 集群广播推送 方案(因全员群发言频率高)。
万人群聊推送策略
对于万人群聊,一般系统的压力就在于消息的扇出(写扩散)。如果按照精准投递的话,我们的消息需要查询redis中心路由,然后将消息投递1w次。而如果用消息广播的形式消息只需要投递一次。由websocket自己进行广播消息的拉取与过滤。

- 1.mq的消息消费模式为集群消费,确保每台websocket都能消消费到所有需要投递的消息。
- 2.对比推送的uid在不在本地连接管理的列表,如果不在,直接丢弃消息,也叫过滤消息。
- 3.如果在本地连接管理,根据uid取出channel,就可以进行消息推送了。
单聊消息多的,用精准投递。
群聊消息多的,用广播消息。
对于抹茶,我们是有个全员群聊,很多个小群聊,和很多的单聊。发言频率最多的场景是全员群。所以抹茶最应该使用集群广播推送的方案。
7. 百万直播间推送方案 (广播推送)
7.1 为什么需要广播推送?
- 场景:直播间、全员群等高并发场景,需向海量用户推送同样的消息。
- 问题 :如果采用 精准投递 方案,Router 需要连接所有 WebSocket 机器,且需要多次 RPC 推送消息,开销大。
- 目标:没有消息的扇出(写扩散)的压力,只需要写一次。
假设在抖音直播的场景下,一个热门的直播间100w人同时在线,大量的礼物,互动消息充斥在直播间,如何通知到每个人。保证消息的即时性,可靠性。
首先这明显更倾向于大群聊的一种场景,如果用精准投递,那么消息的扩散系数就是100w级。如果采用的是集群推送,假设100w的用户需要500台websocket进行连接,那么扩数系数只是500的级别。
但是这个假设是整个平台只有这个直播间,如果平台有更多的直播间。websocket会更多,mq的扩散系数也会更大。

每个方案又都优缺点,而应对极端场景,通常都是方案的组合,找场长避短。很类似于我们后面会提到的推拉结合。
这个场景的方案,我们可以设置一个热门阈值,比如1w。超过1w的直播间,我们会进行直播间升级,升级成热门直播间。热门直播间的websocket单独管理。把直播间用户的websocket连接都统一路由到固定的几百台websocket上。由于目标用户都集中了,也就不需要精准投递了,可以采用广播投递消息到这指定500台机器上。再对应的推送给直播间的观众。
这里其实是精准投递和集群推送的一个结合(你会发现,很多方案都是有优劣的,最后都是结合起来使用扬长避短)
这个方案的核心,就是要能将直播间所有用户通过网关路由到相同同的500台websocket上,有了这个基础,才能用广播消息,那500台websocket都监听同一个topic的广播mq消息。能省下很大的带宽开销。而消息的发送端,需要知道消息究竟是发送到热门直播间的topic进行集群广播 还是普通直播间的精准推送。还是得依赖router服务进行路由推送。
7.2 广播推送的实现
- MQ 实现广播 :使用 消息队列(MQ)的广播模式(如 RocketMQ 的 Topic 广播消费模式),将消息投递到 Topic,所有 WebSocket 服务都订阅该 Topic。
- WebSocket 广播:WebSocket 服务消费 MQ 消息后,对本地所有连接的 Channel 进行遍历推送。
7.3 几个功能的细节:
热门直播间升级
一开始的普通直播间,用户都分散在不同的websocket机器上。等到直播间人数突破阈值1w,就需要开始直播间的热点升级 。这时候服务器检测到直播间需要升级,动态扩缩容 ,启动一系列配套措施(k8s现在已经使用的比较多了)。一系列措施准备好后,相应的配置推送到网关路由机器上。指定以后该直播间的连接路由到我们新启动的50台websocket上。然后对当前在线的所有用户发送断连替换指令。所有在线用户都断开连接,重连的时候会被网关路由到新的websocket上。
优化:对于经常突破1w人的直播间,可以打个标。以后该直播间上线,默认就是热点,省略升级过程。
消息合并
直播间的点赞操作,一般发生在主播求赞的时候,大量人在同一时间段点赞。并且单人在同一时间也快速点赞。可以在客户端对每个人的多次点赞首先进行合并一次(用户a点赞20)。请求到后端后,由于路由已经做好,在每个点赞服务器,可以对多人的点赞再合并一次(用户a+b总点赞40),进行入库。给前端推送的时候,也可以合并推送,不需要每条点赞都推送。每隔1s推送一次直播间点赞总量达到(100w)。
不了解请求合并思想的,可查看《架构之路》的文章请求折叠工具类。
优先级隔离
在100w直播间里,推送的消息会有很多。会导致部分消息到达产生延迟。这就类似push系统,消息应该区分优先级,不要被互相影响。大礼物,和主播发言消息,这些应该独立在一个广播topic里,其他的不重要的消息,可以设置另一个topic,区分优先级,不要影响重要消息。
7.4 RocketMQ Tag 过滤 (实现指定推送)
- 问题:如何实现给特定分组(如年龄、性别)推送?
- 方案 :使用 MQ 的 Tag 过滤 。
- 消息携带 Tag(如
age>=20)。 - WebSocket 服务本地维护 用户 Tag 到 Channel 的映射。
- WebSocket 服务接收到消息后,根据消息的 Tag 找到对应的 Channel 列表进行推送。
- 消息携带 Tag(如
8. 消息可靠性保证
8.1 消息发送的可靠性 (客户端 -> 服务端)
- 发送方 :发送消息后,需要等待接收方(服务端)的 ACK(确认) 消息。
- 应用层 ACK:由于 WebSocket 底层是 TCP 协议,收发报文不关联,需要通过一个唯一标识,标识推出去的消息是用来响应上一个接收的消息的。发送方客户端也需要等待 ACK 的到来。
MallChat 方案:MallChat 使用 HTTP 来发送消息,通过返回的标识,判断是否发送成功即可。
- 明确失败(ACK 失败):可能是业务校验问题,提示用户即可。
- 超时:底层帮助自动重发,确保发送可靠。
8.2 消息推送的可靠性 (服务端 -> 客户端)
推送可靠性旨在保证服务端消息入库成功后,能够到达对应的消息接收方。
- 持久化 :为了保证严格的可靠性,推送给每个人的 ACK 都需要入库,写到每个人的 消息表/信箱 持久化,接收到 ACK 后,修改消息状态。
- 定时重试:如果信箱没有收到 ACK,说明消息没有到达接收端,需要进行重新推送。可靠的前提是信箱是持久化的,且支持定时任务不断重试。
根据文档内容,提取的 MySQL 表结构如下(含字段定义、注释及核心索引设计):
表结构设计
核心表关系
核心表共 7 张,分为「基础会话层」「消息层」「成员层」三类,通过 room_id(房间唯一标识)作为核心关联键,屏蔽单聊/群聊差异,实现统一会话管理。
关系图结构化说明


c
erDiagram
%% 1. 房间表(核心抽象层,屏蔽单聊/群聊差异,统一会话标识)
ROOM {
bigint(20) room_id PK "房间唯一标识(主键,全局唯一)"
int(11) type "房间类型:1=单聊/2=群聊/3=热点群聊"
tinyint(1) is_hot "是否为热点群聊:1=是/0=否(用于区分读写扩散策略)"
json ext_info "扩展信息(群聊存头像/名称,单聊无额外信息)"
bigint(20) last_msg_id "房间最新消息ID(用于会话排序与聚合)"
datetime(3) create_time "房间创建时间(毫秒精度,确保时序性)"
datetime(3) update_time "房间更新时间(热点状态/最新消息变更触发)"
}
note for ROOM "核心抽象表,解耦单聊/群聊逻辑,所有会话相关表通过room_id关联"
%% 2. 单聊扩展表(与ROOM一对一,仅存单聊专属信息)
SINGLE_CHAT {
bigint(20) id PK "单聊扩展表主键"
bigint(20) room_id FK "关联ROOM.room_id(一对一,绑定单聊对应的房间)"
bigint(20) uid1 "单聊双方中UID较小者(用于生成唯一room_key)"
bigint(20) uid2 "单聊双方中UID较大者"
varchar(64) room_key UK "单聊房间唯一标识(格式:uid1_uid2,避免重复创建)"
datetime(3) create_time "单聊房间创建时间"
}
note for SINGLE_CHAT "单聊专属扩展表,不存储消息核心逻辑,仅维护单聊双方关系"
%% 3. 群聊扩展表(与ROOM一对一,仅存群聊专属信息)
GROUP_CHAT {
bigint(20) id PK "群聊扩展表主键"
bigint(20) room_id FK "关联ROOM.room_id(一对一,绑定群聊对应的房间)"
varchar(64) group_name "群聊名称"
varchar(255) avatar "群聊头像URL(存储于OSS等对象存储)"
bigint(20) owner_uid "群主UID(冗余字段,加速群主查询,避免关联群成员表)"
datetime(3) create_time "群聊创建时间"
datetime(3) update_time "群聊信息更新时间(名称/头像变更触发)"
}
note for GROUP_CHAT "群聊专属扩展表,维护群聊基础信息,群成员关系单独存储于GROUP_MEMBER"
%% 4. 群成员表(与GROUP_CHAT一对多,管理群成员角色与关联)
GROUP_MEMBER {
bigint(20) id PK "群成员表主键"
bigint(20) group_id FK "关联GROUP_CHAT.id(即群聊对应的扩展表记录,绑定群聊)"
bigint(20) uid "群成员UID(关联用户表,文档未明确定义用户表,此处为隐含关联)"
tinyint(1) role "成员角色:0=普通成员/1=管理员/2=群主(用于权限控制)"
datetime(3) join_time "成员加入群聊的时间"
}
note for GROUP_MEMBER "群成员关系表,支持多角色管理,通过联合索引优化群主/管理员查询"
note over GROUP_MEMBER "联合唯一索引:(group_id, uid) 避免重复入群;联合索引:(group_id, role) 快速筛选群主/管理员"
%% 5. 消息表(与ROOM一对多,存储所有会话的消息内容,兼容多类型消息)
MESSAGE {
bigint(20) id PK "消息唯一ID(全局单调递增,用于时序性排序与游标翻页)"
bigint(20) room_id FK "关联ROOM.room_id(绑定消息所属的会话房间)"
bigint(20) from_uid "消息发送者UID(关联用户表,隐含)"
int(11) type "消息类型:1=文本/2=图片/3=文件/4=语音(用于客户端解析展示)"
varchar(5000) content "文本消息内容(非文本类型可空,详情存于extra)"
json extra "扩展信息(图片/文件URL、语音时长等非文本消息详情)"
bigint(20) reply_msg_id "回复的目标消息ID(可选,用于消息回复功能)"
int(11) status "消息状态:0=正常/1=撤回/2=删除(用于消息生命周期管理)"
datetime(3) create_time "消息发送时间(毫秒精度,辅助时序排序)"
datetime(3) update_time "消息状态更新时间(撤回/删除操作触发)"
}
note for MESSAGE "核心消息存储表,通过type+extra字段兼容多类型消息,无需拆分表结构"
note over MESSAGE "联合索引:(room_id, create_time) 优化会话内消息按时间查询性能"
%% 6. 会话表(用户收信箱,与ROOM一对多,记录用户-会话交互状态,替代传统收信箱)
CONTACT {
bigint(20) id PK "会话表主键"
bigint(20) uid "收信人UID(关联用户表,隐含,标识会话所属用户)"
bigint(20) room_id FK "关联ROOM.room_id(绑定用户参与的会话房间)"
bigint(20) last_msg_id "该会话的最新消息ID(关联MESSAGE.id,用于会话列表排序)"
datetime(3) read_time "用户最后阅读时间(用于已读未读统计,替代每条消息的ack记录)"
datetime(3) active_time "会话活跃时间(用于会话列表倒序排序,新消息触发更新)"
datetime(3) create_time "会话创建时间(用户首次加入房间/单聊初始化触发)"
datetime(3) update_time "会话状态更新时间(阅读/新消息/房间信息变更触发)"
}
note for CONTACT "优化后的用户收信箱,通过read_time记录阅读时间线,避免写扩散导致的存储爆炸"
note over CONTACT "联合唯一索引:(uid, room_id) 避免同一用户重复创建同一会话;联合索引:(uid, active_time) 优化会话列表排序;联合索引:(room_id, read_time) 加速已读未读统计"
%% 7. 热点群聊信箱(可选,独立存储热点群聊会话,减少写扩散开销)
HOT_ROOM_INBOX {
bigint(20) id PK "热点群聊信箱主键"
bigint(20) room_id FK "关联ROOM.room_id(绑定热点群聊房间)"
bigint(20) last_msg_id "热点群聊最新消息ID(关联MESSAGE.id,用于聚合查询)"
datetime(3) update_time "最新消息更新时间(热点群聊新消息触发)"
}
note for HOT_ROOM_INBOX "热点群聊专属会话存储,避免写扩散到每个用户的CONTACT表,通过聚合层与普通会话合并"
%% 表间关联关系(解耦设计,核心表独立,扩展表按需关联)
ROOM ||--|| SINGLE_CHAT : "一对一(单聊扩展,不影响ROOM核心逻辑)"
ROOM ||--|| GROUP_CHAT : "一对一(群聊扩展,不影响ROOM核心逻辑)"
GROUP_CHAT ||--o{ GROUP_MEMBER : "一对多(一个群聊包含多个成员,成员仅属于一个群聊)"
ROOM ||--o{ MESSAGE : "一对多(一个房间包含多条消息,消息仅属于一个房间)"
ROOM ||--o{ CONTACT : "一对多(一个房间关联多个用户的会话,用户会话仅对应一个房间)"
ROOM ||--o{ HOT_ROOM_INBOX : "一对多(一个热点房间对应一条热点会话记录,仅热点群聊使用)"
MESSAGE ||--o{ CONTACT : "间接关联(通过last_msg_id同步会话最新消息,无强耦合)"
1. 房间表(room)
| 字段名 | 类型 | 说明 |
|---|---|---|
| room_id | bigint(20) | 房间唯一标识(主键),屏蔽单聊/群聊差异,作为会话唯一标识 |
| type | int(11) | 房间类型(单聊/群聊/热点群聊) |
| ext_info | varchar/JSON | 扩展信息(如群聊头像、群名称;单聊无额外信息) |
| is_hot | tinyint(1) | 是否为热点群聊(1=是,0=否) |
| last_msg_id | bigint(20) | 房间最新消息 ID(用于排序和聚合) |
| create_time | datetime(3) | 房间创建时间 |
| update_time | datetime(3) | 房间更新时间(如热点状态变更、最新消息更新) |
| 索引 | - | 联合索引:type + is_hot(快速筛选热点群聊);room_id(主键索引) |
2. 单聊扩展表(single_chat)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint(20) | 主键 |
| room_id | bigint(20) | 关联房间表的 room_id(唯一关联,一对一) |
| uid1 | bigint(20) | 单聊双方中 UID 较小的一方(用于生成唯一房间标识) |
| uid2 | bigint(20) | 单聊双方中 UID 较大的一方 |
| room_key | varchar(64) | 唯一标识(格式:uid1_uid2,避免重复创建房间) |
| create_time | datetime(3) | 单聊房间创建时间 |
| 索引 | - | 联合唯一索引:room_key;关联索引:room_id |
3. 群聊扩展表(group_chat)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint(20) | 主键 |
| room_id | bigint(20) | 关联房间表的 room_id(唯一关联,一对一) |
| group_name | varchar(64) | 群名称 |
| avatar | varchar(255) | 群头像 URL |
| owner_uid | bigint(20) | 群主 UID(冗余字段,加速查询) |
| create_time | datetime(3) | 群创建时间 |
| update_time | datetime(3) | 群信息更新时间 |
| 索引 | - | 关联索引:room_id;普通索引:owner_uid |
4. 群成员表(group_member)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint(20) | 主键 |
| group_id | bigint(20) | 关联群聊扩展表的 id(即群聊对应的 room_id 关联) |
| uid | bigint(20) | 群成员 UID |
| role | tinyint(1) | 成员角色(0=普通成员,1=管理员,2=群主) |
| join_time | datetime(3) | 加入群聊时间 |
| 索引 | - | 联合索引:group_id + role(快速筛选群主/管理员);联合唯一索引:group_id + uid(避免重复加入) |
5. 消息表(message)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint(20) | 消息唯一标识(主键,全局单调递增,用于时序性和游标翻页) |
| room_id | bigint(20) | 消息所属房间 ID |
| from_uid | bigint(20) | 发送者 UID |
| type | int(11) | 消息类型(文本/图片/文件/视频/语音等) |
| content | varchar(5000) | 文本消息内容(非文本类型可存空,详情放 extra) |
| extra | JSON | 扩展信息(如图片/文件的 OSS URL、语音时长等) |
| reply_msg_id | bigint(20) | 回复的消息 ID(文本消息专用,可冗余到 extra) |
| gap_count | int(11) | 消息间隔计数(文本消息专用,可冗余到 extra) |
| status | int(11) | 消息状态(0=正常,1=撤回,2=删除) |
| create_time | datetime(3) | 消息发送时间(毫秒精度) |
| update_time | datetime(3) | 消息状态更新时间 |
| 索引 | - | 联合索引:room_id + create_time(会话消息查询);主键索引:id |
6. 会话表(contact,即优化后的用户收信箱)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint(20) | 主键 |
| uid | bigint(20) | 收信人 UID |
| room_id | bigint(20) | 关联房间表的 room_id(非热点群聊的会话关联) |
| last_msg_id | bigint(20) | 该房间最新消息 ID(用于排序) |
| read_time | datetime(3) | 最后阅读时间(用于已读未读统计) |
| active_time | datetime(3) | 会话活跃时间(用于会话列表排序) |
| create_time | datetime(3) | 会话创建时间(首次加入房间/单聊时生成) |
| update_time | datetime(3) | 会话更新时间(如最新消息接收、阅读状态变更) |
| 索引 | - | 联合索引:uid + active_time(会话列表查询);联合索引:room_id + read_time(已读未读统计);联合唯一索引:uid + room_id(避免重复会话) |
7. 热点群聊收信箱(hot_room_inbox,可选,独立存储热点群聊会话)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint(20) | 主键 |
| uid | bigint(20) | 群成员 UID |
| room_id | bigint(20) | 热点群聊房间 ID |
| last_msg_id | bigint(20) | 热点群聊最新消息 ID(用于聚合排序) |
| read_time | datetime(3) | 最后阅读时间(用于已读未读统计) |
| active_time | datetime(3) | 会话活跃时间(用于聚合排序) |
| create_time | datetime(3) | 加入热点群聊时间 |
| update_time | datetime(3) | 会话更新时间 |
| 索引 | - | 联合索引:uid + active_time(聚合查询);联合唯一索引:uid + room_id |
核心设计说明
- 抽象房间表 :通过
room表屏蔽单聊/群聊差异,single_chat和group_chat作为扩展表,降低表结构冗余。 - 会话表优化 :
contact表替代传统收信箱,仅记录用户-房间的阅读时间线,避免写扩散导致的存储爆炸。 - 热点群聊隔离 :热点群聊通过
room.is_hot标识,可独立存储到hot_room_inbox或缓存到 Redis,减少写扩散开销。 - 索引设计:所有核心查询场景(会话列表、消息查询、已读未读统计)均设计联合索引,保证查询性能。