IM通信项目中消息的设计与思考

消息如何存储的?

消息处理流程

优化

用户上线读取离线消息时,读取的是3天内的,如果用户从此再也不上线或者很久才上线,那么DB中就多了很多无用的消息,可以将离线消息存储到redis中,并设置过期时间为3天,当用户上线时,首先读取redis中的消息,再持久化到DB。

离线消息表:redis 消息表:mysql

redis在这期间如果宕机导致消息丢失怎么办?

  • 从redis入手

    • 持久化RDB和AOF

    • 采用主从复制和哨兵模式,提高可用性

但这样做的成本未免有些大

  • 从db入手

    • 可以考虑在表中增加一个字段is_read表示是否已读,0表示未读,1表示已读

      • 已读:用户上线且点击聊天框 。未读:用户未上线 or 用户上线了但并没有点击聊天框

那么处理流程为:

A发消息:消息在redis中存储3天,在mysql中也存储,is_read为0

B上线,超过3天:redis中不存在,去db中读取; 未超过3天:从redis中读取,并将is_read置位1

这样设计间接避免了redis宕机导致消息丢失的问题,并且还可以额外实现已读未读功能。

图片,视频等消息的处理

用户发送消息的类型有文本消息,图片,视频等。

  • 如果是文本消息,不存在下载等问题

  • 如果是图片,视频,必然有一个下载的问题,用户可以选择是自动下载?还是手动下载(点击之后下载)?

表的设计

如果是高并发的流量下,消息的处理流程怎么设计?

自增id就不适用了,改用分布式ID,例如雪花算法生成messageID生成消息,再引入MQ去存储消息,当对方(该消息的消费者)上线时,去读取消息。

如何将消息写入到mysql?

专门引入一个消费者异步的去将消息批量插入到数据表中。

消息顺序

在一个会话中,通常认为消息是有顺序的,但是一旦经过网络,由于网络传输的问题,导致消息重发,从而接收到的消息看起来顺序不一致。

每个会话中,根据时间戳排序 or 根据序列号排序

时间戳生成

如果是客户端的时间戳,但是每个发送方的本地时钟不同,导致生产的时间戳不一致。

如果是服务端的时间戳

单机:时钟固定,时间戳可以保证顺序

集群:需要保证服务器之间的时钟一致

但仍可能避免不了 时钟漂移的问题,且在分布式环境下,网络时延误差更大

序列号(messageID)生成

客户端本地去维护 序列号+服务端的时间戳

本人发送的消息是存储在客户端的,seq一定是递增保证顺序的,即使time相同(同一时刻发送消息),也可以通过seq去保证消息的顺序。

服务端接收到msg后,返回一个seq+1的序列号,从而保证了消息的顺序性

如果是在群聊环境下,每个用户的seq不同,消息顺序未必得到保障,如何解决

seq改为在群聊环境中服务端生成一个全局的单调递增的seq

方式 :数据库中的id 自增(messageID),redis incr自增 , 分布式Id雪花算法

目前我采用数据库自增id作为序列号,但是并没有区分单聊和群聊。但它很简单的保证了消息的顺序和不重复。

总结

单聊:客户端生成seq序列号+服务端生成的time时间戳,seq和time增加保证顺序,先根据time排序,再根据seq排序。如果time相同,根据seq排序。

seq可以不是全局唯一,因为已经有time去排序了,所以客户端发送msg时seq每次可以自行指定。

群聊:服务端生成一个seq,每个群sessionId(群的sessionId对应一个groupId)维护一个全局的seq,保证该群内的msg的seq从1严格递增。

但是如果是这样的话,表结构可能要发生改变,可能要新增一个seqID字段,因为单聊和群聊区分开了,就不能用messageID去区分了。

那么流程为:

单聊

客户端

  1. 客户端在发送消息时,生成一个递增的 seq 序列号。这个 seq 序列号可以是每次会话开始时的某个固定值开始,然后递增。

  2. 客户端同时获取当前的时间戳 time,并将其与 seq 一起发送给服务端。

服务端

  1. 服务端接收到消息后,将 seqtime 一起存储到 chat_message 表中。

  2. 当查询消息时,服务端首先根据 sessionId(会话ID)和 time 对消息进行排序,然后再根据 seq 对时间相同的消息进行排序。这样可以确保在相同的时间戳下,按照 seq 的顺序来展示消息。

群聊

服务端

  1. 对于每个群聊(由 sessionId 标识),服务端维护一个全局的 seq 序列号。

  2. 当有消息发送到群聊时,服务端递增该 sessionId 对应的 seq 序列号,并将新的 seq 与消息一起存储到 chat_message 表中。

  3. 在查询群聊消息时,服务端直接根据 sessionIdseq 对消息进行排序,因为 seq 已经是全局递增的,所以无需再根据时间戳排序。

消息重复

messageId是主键自增唯一的,并且数据库正确地管理了主键的自增行为,那么在单个数据库实例中,由于messageId的唯一性约束,消息重复是不可能发生的

为什么看起来消息会重复?

网络问题导致重复发送。发送者发送消息后由于网络超时的原因,发送者认为消息没发出去,就会重复发送同一条消息,最终导致发送了多条相同的消息,但其实这些msg的messageId并不相同。看起来像是重复

前边的保证消息顺序的方案,如果是在高并发的流量下,我们采用了分布式的方案,在分布式下,messageId为雪花算法生成,那么如何保证消息不重复?

uuid去重:每次发送消息时,都生成一个uuid。将uuid保存在redis中或者hashset,每次发送消息时,去redis或者hashset中检测是否存在,如果存在,说明重复发送了。如果不存在,说明为第一次发送!

每次登陆后,历史消息是怎么得到的?

根据lastofftime(最后一次登录时间),获取3天内的消息

离线消息的处理

如何判断是离线消息?

contactId为自己加入的所有群的id和自己的id,然后根据时间差查出离线消息

selectList:

查询条件:

send_time>=#{query.lastReceiveTime} 最后一条消息的发送时间>最后一次离线的时间

 <!--额外条件-->
 <if test="query.lastReceiveTime!=null">
    and send_time>=#{query.lastReceiveTime}
 </if>
 <if test="query.contactIdList!=null and query.contactIdList.size()>0 ">
        and contact_id in
        <!--foreach外边记得加()-->
        (<foreach collection="query.contactIdList" separator=","item="item">
               #{item}
        </foreach>)
 </if>

原sql语句

 SELECT
     message_id,
     session_id,
     message_type,
     message_content,
     send_user_id,
     send_user_nick_name,
     send_time,
     contact_id,
     contact_type,
     file_size,
     file_name,
     file_type,
 STATUS 
 FROM
     chat_message 
 WHERE
     send_time >= '1720475060883' 
     AND contact_id IN ( ' G67490845307', 'G90495472339', 'G96282038082', 'U47379577422' )

索引

执行SQL后走了 send_time索引

image-20240709055103668


sql分页优化

InterviewGuide

如果数据特别多,可以采用分页优化。

前端中,分页是通过滑动条体现分页的,前端会传页码(page)和每页记录数(pageSize)。

后端接收到数据后,根据pagepageSize计算出start和end从而实现分页

start 代表 第几页开始查询,end表示每页多少数据

 this.start = (pageNo - 1) * pageSize;
 this.end = this.pageSize;

sql:

 SELECT
     message_id,
     session_id,
     message_type,
     message_content,
     send_user_id,
     send_user_nick_name,
     send_time,
     contact_id,
     contact_type,
     file_size,
     file_name,
     file_type,
 STATUS 
 FROM
     chat_message 
 WHERE
     send_time >= '1720475060883' 
     AND contact_id IN ( ' G67490845307', 'G90495472339', 'G96282038082', 'U47379577422' ) 
 ORDER BY  send_time 
 LIMIT 1,2

仍然走了索引去加快查询效率

此SQL的执行过程

由于有send time 和contact id两个索引,先根据send time 索引去查找到主键,然后根据contact id去筛选主键(如果数据库认为使用contact_id索引能够进一步提高性能(例如,当IN列表中的值在contact_id索引中分布得很集中时),它可能会在回表后使用contact_id索引来进一步过滤结果。这种策略通常被称为“索引条件推送”)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值