【需求由来】上一篇文章讲的比较多的是一对一消息传递:《微信为什么不丢消息?》《http如何像tcp一样实时的收消息?》群聊是多人社交的基本需求。无论是QQ群还是微信群,群里有群友发消息:(1)在线群成员最晚可以收到消息(2)离线群成员登录后可以收到消息由于“消息风暴扩散系数”(概念见《QQ状态同步究竟是推还是拉?》)的存在,群组消息的复杂度远高于一对一消息。群消息的实时性、可访问性和离线消息传递是今天要讨论的核心话题。【普通群消息流程】在开始讲群消息传递流程之前,先介绍一下两个群服务的核心数据结构:群成员表:用来描述一个群有多少成员t_group_users(group_id,user_id)group离线消息表:离线消息t_offine_msgs(user_id,group_id,sender_id,time,msg_id,msg_detail)用于描述一个群成员的业务场景示例:(1)一个群中有5个成员,包括x,A,B、C、D,和成员x发送消息(2)成员A、B在线,希望实时收到消息(3)成员C、D不在线,希望以后拉取离线消息到系统架构:(1)Clients:x,A,B,C,D,共5个client用户(2)Server(2.1)所有模块和服务抽象为server(2.2)所有用户在线状态抽象并存储在一个高可用缓存中(2.3)所有的数据信息,比如群组成员,group离线消息抽象存储在db中。典型的群消息下发流程,如图中步骤1-4所示:第一步:群消息发送者x向服务器发送群消息第二步:服务器去db查询有多少用户group(x,A,B,C,D)Step3:服务器去缓存中查询这些用户的在线状态Step4:对于群内的在线用户A和B,群消息服务器发送真实的-时间推送第五步:对于C、D群下线的用户,群消息服务器进行离线存储。典型的群离线消息拉取流程如图1-3步骤:Step1:离线消息拉取器C从服务器拉取群离线消息Step2:服务端从db中获取群离线消息Pullofflinemessages并返回组用户C第三步:服务器从db中删除组用户C的组下线消息。群消息的内容,很多线下用户都存了很多份。假设群内有200个用户离线,则200份离线消息是多余的,这大大增加了数据库的存储压力。【群消息优化1:减少存储容量】为了减少离线消息的冗余,增加了一个群消息表,用于存储所有群消息的内容。离线消息表只存储用户的群组离线消息msg_id,可以大大提高减少数据库的冗余存储容量群组消息表:用于存储一个群组中所有消息内容t_group_msgs(group_id,sender_id,time,msg_id,msg_detail)群离线消息表:优化后只存储msg_idt_offine_msgs(user_id,group_id,msg_id)本次优化后,对群在线消息发送做了一些修改:第三步:发送在线群消息前,群消息的内容必须首先存储。第六步:每次存储离线消息时,只存储msg_id,而不是每次存储msg_detail拉取离线消息时,也做相应的修改:第一步:先拉取所有离线消息msg_id第三步:再拉取msg_detail根据msg_id第五步:删除下线msg_id问题像一对一消息一样存在和发送一样:(1)在线消息传递可能会导致消息丢失,比如服务器重启,路由器丢包,客户端崩溃(2)离线消息拉取也可能导致消息丢失,原因同上和一对一消息的可靠传递一样,可以加上应用层的ACK来保证群消息到达。【群消息优化2:应用层ACK】应用层ACK优化后,群在线消息发送发生了一些变化:第3步:消息msg_detail存入群消息表后,不管用户是否在线与否,先将Step6中的msg_id存入离线消息表:B将自己的离线消息发送给删除组离线消息对应的msg_id也是一样的:第一步:先拉取msg_id第三步:再拉取msg_detail步骤五:***应用层ACK步骤六:服务端只能删除离线消息收到应用层ACK后的消息表中msg_id的问题(1)如果拉取消息但没有应用层ACK,是否会收到重复的消息?答:可以,可以在客户端去重。对于重复的msg_id,对用户来说是不利的。显示,以免影响用户体验(2)对于每条离线消息,虽然只存储了msg_id,但是每个用户的每条离线消息都会在数据库中保存一条记录。有什么办法可以减少离线消息记录的数量吗?关于什么?【群消息优化3:离线消息表】优化离线消息表。实际上,对于一个群用户来说,在注销后的离线期间,是一定不能接收到所有群消息的。每条离线消息存储一个离线msg_id,只需要存储上次拉取离线消息的时间(或msg_id),下次登录时拉取之后的所有群消息,无需存储离线消息msg_id大家未拉取的群成员表:用于描述一个群有多少个成员,以及每个成员发送ack的群消息的msg_id(或时间)t_group_users(group_id,user_id,last_ack_msg_id(last_ack_msg_time))group消息表:用于存放一个群内所有消息内容,不变t_group_msgs(group_id,sender_id,time,msg_id,msg_detail)群下线消息表:不再需要线下消息表优化,群上线消息传递流程:Step3:messagemsg_detail存储在群消息表中,不再需要操作离线消息表(优化前需要将msg_id插入到离线消息表中)Step7:Afteron线路用户A和B在应用层收到ACK,更新last_ack_msg_id即可(优化前需将离线消息表中的msg_id删除)。拉群下线消息的过程也类似:Step1:Pull获取离线消息Step3:ACKofflinemessageStep4:updatelast_ack_msg_id存在的问题由于“消息风暴扩散系数”的存在,假设一个群有500个用户,“每个”群消息会变成500个应用层ACK,会有一个对服务器的影响很大。有什么办法可以减少ACK请求的数量吗?【群消息优化4:批量ACK】由于“消息风暴扩散系数”的存在,如果每条群消息都ACK,将会对服务器造成巨大的伤害。为了减少ACK请求量,很容易想到批量ACK的方法。批量ACK有两种方式:(1)每收到N个群消息,请求量减为1/N(2)每隔时间间隔T进行一次群消息ACK,也可以达到类似的效果。批量ACK新问题可能会导致:用户在ACK组消息前登出,这样下次登录会拉取Duplicateofflinemessage解决方法msg_id去重,不显示给用户,保证良好的用户体验,也可能有离线消息过多的问题:pull太慢解决方法:分页拉取(pullondemand),分页拉取具体内容在“为什么微信不丢失离线消息”一章有详细介绍,不再展开在这里(详见《微信为啥不丢“离线消息”?》)。【总结】群消息还是很有意思的,比如无障碍、实时、离线消息、消息风暴传播等等,做一个总结:(1)不管是群在线消息还是群离线消息,ACK的应用层是可达性的保证(2)只保存一份群消息,不再为每个用户保存离线群msg_id,只保存一个最新ack的群消息id/时间(3)为了减少消息风暴,批量ACK(4)如果收到重复消息,需要去重msg_id,让用户感觉不到(5)如果离线消息太多,可以分页拉取(按需拉动)来优化
