10

群消息已读回执(这个屌),究竟是推还是拉?

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MjM5ODYxMDA5OQ%3D%3D&%3Bmid=2651966192&%3Bidx=1&%3Bsn=e50edd636a55611b1b285e60859315d3
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

每当发出一条微信消息,都希望对方尽快看到,并尽快回复,但始终不知道对方是否阅读。

每当收到一条不能立马回复的微信消息,都默默返回,假装没看见。

画外音:不想回复的人,唉,你只是个好人。

微信用于个人社交,产品设计上,在线状态,强制已读回执都有可能暴露个人隐私,故微信并无相关功能。

钉钉用于商务交流,其“强制已读回执”功能,让职场人无法再“假装不在线”,“假装没收到”。

有甚者,钉钉的群有“强制已读回执”功能,你在群里发出的消息,能够知道谁读了消息,谁没有读消息。

群消息的流程如何,接收方如何确保收到群消息,发送方如何收已读回执 ,究竟 是拉取,还是推送 ,是今天要聊的话题。

一、群消息投递流程,以及可达性保证

大家一起跟着楼主的节奏,一步一步来看群消息怎么设计。

核心问题1:群消息,只存一份?还是,每个成员存一份?

存一份 ,为每个成员设置一个群消息队列,会有大量数据冗余,并不合适。

核心问题2:如果群消息只存一份,怎么知道每个成员读了哪些消息?

:可以利用群消息的偏序关系, 记录每个成员的 last_ack_msgid (last_ack_time) ,这条消息之前的消息已读,这条消息之后的消息未读。该方案意味着,对于群内的每一个用户,只需要记录一个值即可。

解答上述两个核心问题后,很容易得到群消息的 核心数据结构

群消息表 :记录群消息。

group_msgs(msgid, gid, sender_uid, time, content);

各字段的含义为:消息ID,群ID,发送方UID,发送时间,发送内容。

群成员表 :记录群里的成员,以及每个成员收到的最后一条群消息。

group_users(gid, uid, last_ack_msgid);

各字段的含义为:群ID,群成员UID,群成员最后收到的一条群消息ID。

在核心数据结构设计完之后,一起来看看群 消息发送的流程

业务场景:

(1)一个群中有A, uid1, uid2, uid3四名成员;

(2)A, uid1, uid2在线,期望实时收到在线消息;

(3)uid3离线,期望未来拉取到离线消息;

2aaABzN.jpg!mobile

其整个消息发送的流程1-4如上图:

(1)A发出群消息;

(2)server收到消息后,一来要将 群消息落地 ,二来要 查询群里有哪些群成员 ,以便实施推送;

(3)对于群成员,查询在线状态;

(4)对于在线的群成员,实施推送;

这个流程里,只要第二步消息落地完成,就能保证群消息不会丢失。

核心问题3:如何保证接收方一定收到群消息?

:各个收到消息后,要 修改各群成员的 last_ack_msgid ,以告诉系统,这一条消息确认收到了。

在线消息,离线消息的 last_ack_msgid 的修改,又各有不同。

3aYrEbj.jpg!mobile

对于 在线的群友 ,收到群消息后, 第一时间会ack,修改 last_ack_msgid

JrYfqiN.jpg!mobile

对于 离线的群友 ,会在 下一次登录时 ,拉取未读的所有群离线消息,并 last_ack_msgid 修改 为最新的一条消息。

核心问题4:如果ack丢失,群友会不会拉取重复的群消息?

,可以根据msgid在 客户端本地做去重 ,即使系统层面收到了重复的消息,仍然可以保证良好的用户体验。

上述流程,只能确保接收方收到消息,发送方仍然不知道哪些人在线阅读了消息,哪些人离线未阅读消息,并没有实现已读回执, 那已读回执会对系统设计产生什么样的影响呢?

二、已读回执流程

对于发送方发送的任何一条群消息,都需要知道,这条消息有多少人已读多少人未读,就 需要一个基础表来记录这个关系

消息回执表 :用来记录消息的已读回执。

msg_acks(sender_uid, msgid, recv_uid, gid,if_ack);

各字段的含义为:发送方UID,消息ID,回执方UID,群ID,回执标记。

增加了已读回执逻辑后,群消息的流程会有细微的改变。

aAZZZjB.jpg!mobile

步骤二,server收到消息后,除了要:

(1)将群消息落地;

(2)查询群里有哪些群成员,以便实施推送;

之外,还需要:

(3)插入每条消息的初始回执状态;

M7bYbqy.jpg!mobile

接收方修改 last_ack_msgid 的流程,会变为:

(1)发送ack请求;

(2)修改 last_ack_msgid ,并且,修改已读回执if_ack状态;

(3)查询发送方在线状态;

(4)向发送方实时推送已读回执(如果发送方在线);

如果发送方不在线,ta会在下次登录的时候:

(5)从关联表里拉取每条消息的已读回执;

这里的初步结论是:

(1)如果 发送方在线 ,会实时被 推送 已读回执;

(2)如果 发送方不在线 ,会在下次在线时 拉取 已读回执;

三、流程优化方案

再次详细的分析下,群消息已读回执的“消息风暴扩散系数”,假设每个群有200个用户,其中20%的用户在线,即40各用户在线。群用户每发送一条群消息,会有:

(1)40个消息,通知给群友;

(2)40个ack修改last_ack_msgid,发给服务端;

(3)40个已读回执,通知给发送方;

可见,其 消息风暴扩散系数非常之大

同时:

(1)需要存储40条ack记录;

群数量,群友数量,群消息数量越来越多之后, 存储也会成为问题

是否有优化方案呢?

群消息的推送,能否改为接收方轮询拉取?

不能 ,消息接收,实时性是核心指标。

对于 last_ack_msgid 的修改,真的需要每个群消息都进行ack么?

其实不需要 ,可以 批量ack ,累计收到N条群消息(例如10条),再向服务器发送一次 last_ack_msgid 的修改请求,同时修改这个请求之前所有请求的已读回执,这样就能将40个发送给服务端的ack请求量,降为原来的1/10。

会带来什么副作用?

last_ack_msgid 的作用是,记录接收方最近新取的一条群消息,如果不实时更新,可能导致, 异常退出时 ,有一些群消息没来得及更新 last_ack_msgid ,使得下次登陆时, 拉取到重复的群消息 。但这不是问题,客户端可以根据msgid去重, 用户体验不会受影响

发送方在线时,对于已读回执的发送,真的需要实时推送么?

其实不需要 ,发送方每发一条消息,会收到40个已读回执,采用 轮询拉取 (例如1分钟一次,一个小时也就60个请求),可以大大降低请求量。

画外音:或者直接放到应用层keepalive请求里,做到0额外请求增加。

会带来什么副作用?

已读回执更新不实时 ,最坏的情况下,1分钟才更新回执。当然,可以根据性能与产品体验来折衷配置这个轮询时间。

如何降低数据量?

答:回执数据不是核心数据

(1)已读的消息,可以进行 物理删除 ,而不是标记删除;

(2)超过N长时间的回执, 归档或者删除 掉;

四、总结

对于群消息已读回执,一般来说:

(1)如果发送方 在线 ,会实时被 送已读回执;

(2)如果发送方 不在线 ,会在下次在线时 取已读回执;

如果要对进行优化,可以:

(1)接收方累计收到N条群消息再 批量ack

(2)发送方 轮询拉取 已读回执;

(3) 物理删除 已读回执数据,定时删除或归档非核心历史数据;

推还是拉? 任何脱离业务的架构设计都是耍流氓。

r6NBFbA.jpg!mobile

架构师之路-分享技术思路

若有收获,随手 发。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK