消息如何存储的?
消息处理流程
优化
用户上线读取离线消息时,读取的是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去区分了。
那么流程为:
单聊
客户端:
客户端在发送消息时,生成一个递增的
seq
序列号。这个seq
序列号可以是每次会话开始时的某个固定值开始,然后递增。客户端同时获取当前的时间戳
time
,并将其与seq
一起发送给服务端。服务端:
服务端接收到消息后,将
seq
和time
一起存储到chat_message
表中。当查询消息时,服务端首先根据
sessionId
(会话ID)和time
对消息进行排序,然后再根据seq
对时间相同的消息进行排序。这样可以确保在相同的时间戳下,按照seq
的顺序来展示消息。群聊
服务端:
对于每个群聊(由
sessionId
标识),服务端维护一个全局的seq
序列号。当有消息发送到群聊时,服务端递增该
sessionId
对应的seq
序列号,并将新的seq
与消息一起存储到chat_message
表中。在查询群聊消息时,服务端直接根据
sessionId
和seq
对消息进行排序,因为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索引
sql分页优化
如果数据特别多,可以采用分页优化。
前端中,分页是通过滑动条体现分页的,前端会传页码(page
)和每页记录数(pageSize
)。
后端接收到数据后,根据page
和pageSize
计算出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
索引来进一步过滤结果。这种策略通常被称为“索引条件推送”)