消息时序
理想状态下,客户端和服务端数据是一致的。实际情况,涉及到用户上线或下线。(详见下图)
- 用户在线:服务实时发送消息。
- 用户离线:服务保存消息;用户重新上线后,向服务获取离线消息。
-
群组离线消息数据(分页获取)。
如上图:client 每条消息都是有时序的,像链表一样,串联起来,每个 node 都可以通过 next 指向上一条消息:
- 如果上一条消息 msg_id 是 0,说明当前结点是第一条消息(如上图 msg_id == 1 的消息)。
- 如果上一条消息 msg_id 不是 0,且消息存在于本地,那么消息是连续的,不需要向服务同步(如上图 msg_id == 2 的消息)。
- 如果上一条消息 msg_id 不是 0,但本地消息不存在,那么需要向服务器获取。(如上图 msg_id == 9 的消息)。
终端通过消息链表方式的检查,很容易确认是否需要向服务同步数据。
-
群组未读消息总条数。
从 client 的缓存中提取最新(lastest)的 msg_id,对应消息体有 recv_time。
服务端消息的时序通过 redis 的 sortset 存储的:
key: group_id, score: recv_time, value: msg_id
redis 的 sortset 结构,很容易通过一个 score 获取一个区间的数据总数。
redis 设计
-
sortset 存储存储消息时序
key: group_id, score: recv_time, value: msg_id
-
string 存储消息体
key: msg_id, value: msg_body
因为消息体数量较多,而且活跃时间比较短(因为大部分用户只关心最近接收的消息),所以把它独立出来。便于 timeout 后 redis 能删除节省内存。
-
set 存储未读消息对象
key: uid, member:group_id/send_uid
每个用户都可能有 N 个群组,N 个好友。用户重新上线后,不可能遍历所有好友或群组对象。所以服务在处理离线消息时,需要记录未读消息对象。
database 设计
-
群组和群组成员关系
group_id, uid
-
消息结构
msg_id, group_id, send_uid, recv_uid, recv_time, msg_body
服务存储架构
im 即时通讯,服务是读多写少类型。服务端有三层存储(如下图),通过热点数据的缓存,让服务高效读取。
- msg server 服务进程内存 session 缓存热点数据。
缓存当前活跃的数据:头像信息,用户名称,消息实体等数据,缓存一般 5 - 30 分钟,根据具体的业务需要
- redis 第二层缓存热点数据。
缓存大量的热点数据,减少对 db 的访问频率,缓存时间相对较长,几个月不等。
- database 数据落地。
数据读写时序
写数据
读数据
总结
基于以上分析,群组消息,每个用户发送的消息,不需要针对每个群组成员都存一条到 database,否则千人群组,每个成员存一条数据就是一千条,那么消息的存储就是个大坑。每个用户发送消息 database 只需要存一条即可。通过多级缓存的架构,服务的性能一般体量的消息实时通讯是没有问题的。当然这里面还有很多细节问题需要在实际的业务场景中调优。