当前位置: 首页 > 科技观察

群聊比单聊复杂,为什么这么复杂?

时间:2023-03-19 14:12:52 科技观察

群聊是多人社交的基本需求。群成员在群内发送消息,希望实现:在线的群成员可以第一时间收到消息;离线群成员登录消息后可接收消息;群组消息中离线消息的实时性、可达性、复杂度远高于一对一消息。常见的群发流程是怎样的?集团业务有两个核心数据结构。群成员表:t_group_users(group_id,user_id)画外音:用来描述一个群有多少成员。群组离线消息表:t_offine_msgs(user_id,group_id,sender_id,time,msg_id,msg_detail)画外音:用于描述群组成员的离线消息。业务场景示例:假设一个群里有5个成员x,A,B,C,D,成员x发送一条消息;成员A和B在线,希望实时收到消息;成员C和D下线,期待未来拉下线消息;典型的群消息发送流程,如图中步骤1-4所示:步骤1:群消息的发送者x向服务器发送群消息;Step2:服务器去db查询group中有多少用户(x,A,B,C,D);第三步:服务器在缓存中检查这些用户的在线状态;第四步:对于群内在线用户A、B,群消息服务器实时推送;第五步:对于群内在线用户对于离线用户C和D,群消息服务器进行离线存储;典型的群组离线消息拉取流程如图1-3所示:步骤1:离线消息拉取器C从服务器拉取群组离线消息;steps2:服务端从db中拉取离线消息,返回群组用户C;第三步:服务器从db中删除群用户C的群下线消息;那么,问题来了!对于同一条群消息的内容,多个离线用户似乎存储了很多份。假设群内有200个用户离线,则200份离线消息是多余的,这大大增加了数据库的存储压力。如何优化和减少消息冗余?为了减少离线消息的冗余,增加了一个群消息表来存储所有群消息的内容。离线消息表只存储用户所在组的离线消息msg_id,可以大大减少数据库的冗余存储容量。群消息表:t_group_msgs(group_id,sender_id,time,msg_id,msg_detail)画外音:用于存放一个群内的所有消息内容。群下线消息表需要优化:t_offine_msgs(user_id,group_id,msg_id)画外音:优化后只存msg_id。本次优化后,对群组在线消息发送做了一些修改:第三步:发送在线群组消息前,需要先存储群组消息的内容;第六步:每次存储离线消息时,只存储msg_id,而不是每个用户存储msg_detail;拉取离线消息时,也做相应的修改:第一步:先拉取所有离线消息msg_id;第三步:然后根据msg_id拉取msg_detail;第五步:删除离线msg_id;optimized进程能否保证消息的可达性?例如:在线消息传递中可能会出现消息丢失,如服务器重启、路由器丢包、客户端崩溃等;离线消息拉取也可能出现消息丢失,原因同上;画外音:一对一消息的可靠传递同样是通过添加应用层的ACK来实现的。群发消息呢?对于群消息,如何通过应用层ACK保证消息的可靠传递?应用层ACK优化后,群在线消息发送发生了一些变化:第3步:群消息表存储消息msg_detail后,无论用户在线与否,先将msg_id存储在离线消息表;第六步:上线用户A和B收到群消息后,需要添加应用层ACK来标识消息的到达;第七步:在线用户A和B在应用层ACK后删除自己的离线消息msg_id;对应群下线消息拉取也是一样:第一步:先拉取msg_id;第三步:然后拉取msg_detail;第五步:最后是应用层ACK;第六步:服务端收到应用层ACK后,只能删除离线消息表中的msg_id;if如果取到消息但是应用层ACK没有及时得到,会不会收到重复的消息?好像是,但是可以在客户端去重。对于重复的msg_id,不会显示给用户,以免影响用户体验。对于每条离线消息,虽然只存储了msg_id,但是每个用户的每条离线消息都会在数据库中保存一条记录。有什么办法可以减少离线消息记录的数量吗?对于群组用户,在ta下线后的离线期间内,不得接收到所有群组消息。所有离线消息不需要存储离线msg_id,只需要存储最新拉取到的离线消息的时间(或msg_id),下次登录时可以拉取之后的所有群消息,而大家没有拉取的离线消息msg_id就不用存储了。给群成员表增加一个属性:t_group_users(group_id,user_id,last_ack_msg_id)画外音:用来描述一个群有多少成员,以及每个成员最后一次ack消息的msg_id(或者时间)。群消息表,不变:t_group_msgs(group_id,sender_id,time,msg_id,msg_detail)画外音:仍然用于存放一个群内所有的消息内容。群组离线消息表:不再需要。离线消息表优化后,群在线消息的下发流程:Step3:消息msg_detail存入群消息表后,不再需要操作离线消息表(需要将msg_id插入到群消息表中)优化前离线消息表);第七步:在线用户A和B可以在应用层更新ACK后的last_ack_msg_id(优化前需要将离线消息表中的msg_id删除);群下线消息的拉取流程也类似:第一步:拉取下线消息;第三步:ACK离线消息;第4步:更新last_ack_msg_id;添加ACK机制,保证群消息的可靠传递,假设一个群有500个用户,“每条”群消息都会变成500个应用层ACK,这似乎对服务器造成了巨大的危害影响。有什么办法可以减少ACK请求的数量吗?批量ACK是减少请求量的常用方法。如果每条群消息都被ACK,确实会对服务器造成巨大的影响。为了减少ACK请求量,可以批量处理ACK,批量处理ACK有两种方式:每收到N组消息,就ACK一次,这样请求量就减少到原来的1/N;每间隔T一个群消息ACK可以达到类似的效果;批量ACK可能会引发新的问题:如果用户还没来得及ACK群消息,用户就会退出,这样下次登录好像拉了重复的离线消息,怎么办?客户端根据msg_id去重,不显示给用户,保证良好的用户体验。群里离线消息太多,抓取速度太慢,怎么办?分页拉取(按需拉取),细节不再展开,都是常见的优化方案。总结群消息还是很有意思的。简单总结一下:无论是群在线消息还是群离线消息,应用层的ACK都是可达性的保证;只保存一份群消息,无需为每个用户存储离线群消息。msg_id,只需要存放最近一次ack的一个群消息id/时间;为了减少消息风暴,可以批量ACK;如果收到重复消息,需要去重msg_id,让用户感觉不到;离线消息过多,可以通过分页拉取(On-demandpull)优化;想法比结论更重要,希望大家有所收获。