请选择 进入手机版 | 继续访问电脑版

默认
打赏 发表评论 29
想开发IM:买成品怕坑?租第3方怕贵?找开源自已撸?尽量别走弯路了... 找站长给点建议
IM群聊消息究竟是存1份(即扩散读)还是存多份(即扩散写)?

1、前言


IM的群聊消息,究竟存1份(即扩散读方式)还是存多份(即扩散写方式)?

上一篇文章《IM群聊消息的已读回执功能该怎么实现?》里说,“很容易想到,是存一份”,被网友们骂了,大家争论的很激烈(见下图)。

WechatIMG109.jpg

网友骂的对,任何技术方案,都不是天才般灵感乍现想到的,一定是一个演进迭代,逐步优化的过程。今天就聊一聊,IM群聊消息,为啥只需要存一份

不过,从公开的技术资料来看,微信的群聊消息应该使用的是存多份(即扩散写方式),详细的方案可以在微信团队分享的这篇文章里找到答案:《微信后台团队:微信后台异步消息队列的优化升级实践分享》。

2、本文作者


58同城沈剑.jpg
沈剑:58技术委员会主席,58高级架构师,58到家技术总监。C2C技术部负责人,58技术学院优秀讲师。

沈剑的另外几篇有关IM的文章也值得你去阅读:


3、IM开发干货系列文章


本文是系列文章中的第15篇,总目录如下:


另外,如果您是IM开发初学者,强烈建议首先阅读《新手入门一篇就够:从零开发移动端IM》。

4、更多关于IM群聊的文章


IM系统中的群聊功能,是个很大话题,下面几篇在关群聊的文章您也可以读一读:

>> 更多同类文章 ……

另外,《一套海量在线用户的移动端IM架构设计实践分享(含详细图文)》一文中也包含了群聊的完整设计,如果您设计IM不知从何下手,可以详细地参考此文。

5、最基本的方案:“在线的群友不存储消息,离线的群友才存储”


群信息,用户信息,群成员关系都是基础数据:

group_info(gid, group_info);
user_info(uid, user_info);
group_members(gid, uid);


假设一个群(gid)里有4个成员,其中三个在线(A, uid1, uid2),一个不在线(uid3)。

A发送了一条消息,很容易想到,对于不同的群友消息存多份,每个群友一个队列来存储。但由于在线的用户会实时的收到消息,所以暂定只为离线的用户存储

用户收到的群消息,也是基础数据:

user_msgs(uid,msgid,gid,sender_uid,time,content);


1.jpg

很容易想到,整个群消息的发送流程如上图1-4:

  • 1)发送消息;
  • 2)查询状态;
  • 3)不在线的存储离线;
  • 4)在线的实时推送。

“在线的群友不存储,离线的群友才存储”会带来的问题是,如果第四步发生异常,群友会丢失消息

6、优化的方案:“不管群员是否在线,都要先存储消息”


消息的可达性是聊天系统中最重要的要素(没有之一),故这个方案是不行的,需要优化为“不管是否在线,都要先存储”。

2.jpg

发送群消息的流程优化为,如上图1-4:

  • 1)发送消息;
  • 2)所有人都存一份;
  • 3)查询状态;
  • 4)在线的实时推送。

先将消息落地,能够保证消息可达性,那何时才能删除已经落地的群消息呢?我们继续往下看。

3.jpg

对于在线的群友:收到群消息后,给个ack确认才能删除。

画外音:逻辑删除,还是物理删除,根据业务是否有消息漫游决定。

4.jpg

对于离线的群友:在下次登陆后,拉取完离线消息再给ack确认才能删除

总之:为了保证消息的可达性,不管是在线消息还是离线消息,必须接收方给ack确认,才能删除消息。

7、“不管群员是否在线,都冗余一份群消息”带来的问题


“不管是否在线,都冗余一份群消息”带来的问题是:同一条消息存储了很多次,对磁盘和带宽造成了很大的浪费。

很容易想到的优化是:群消息实体存储一份,用户只冗余消息ID。

5.jpg

故基础数据可以由:

user_msgs(uid,msgid,gid,sender_uid,time,content);
优化为:
group_msgs(msgid,gid,sender_uid,time,content);
user_msgs(uid, msgid, gid);


这个优化,对于消息投递,以及消息删除的核心流程没有影响,几个实践为:

  • 在线用户投递消息实体,ack消息ID;
  • 离线用户先拉取消息ID,再拉取消息实体,再ack消息ID。

如此这般,假如在某个群友A期间,群里陆续发送了N条消息,则user_msgs(uid, msgid, gid)里,会有 uidA -> mid1,mid2, mid3, … midN 等N条离线记录,拉取离线消息时,可以把这N条消息一次性拉取出来,然后再删除:

delete from user_msgs  where msgid in($mid1,$mid2…, $midN) and gid=$gid


8、终级方案:利用群消息的“偏序”特性优雅地实现“只存1份”


然而,群消息具备“偏序”特性,上面的一次性删除完全可以优化为:

delete from user_msgs
where msgid >= $mid1 and gid=$gid


这就意味着,每个用户只需要记录“最近一次收到的消息ID”,而不用记录“所有未收到的消息ID集合”,每当收在线消息ack,以及拉离线消息ack时,只需要更新这个“最近一次收到的消息ID”即可。

于是乎,基础数据可以由:
group_members(gid, uid);
group_msgs(msgid,gid,sender_uid,time,content);
user_msgs(uid, msgid, gid);


优化为:
group_members(gid, uid, last_ack_msgid);
group_msgs(msgid,gid,sender_uid,time,content);
user_msgs(uid, msgid, gid); // 不再需要


6.jpg
即:群消息只存储一份,群友无需冗余任何消息实体,或者消息ID了。

7.jpg
对于在线的群友:收到群消息后,修改这个last_ack_msgid。

8.jpg
对于离线的群友:拉取群消息后,也修改这个last_ack_msgid。

画外音:这里的讨论,仅限于接收方收到了哪些消息,和发送方的已读回执没有关系。(这里指的是作者的上篇文章《IM群聊消息的已读回执功能该怎么实现?》)

9、本文小结


任何架构方案都不是灵光一现,而是逐步迭代优化产生的:

  • 方案1:群聊消息存多份,只存在线,消息容易丢;
  • 方案2:群聊消息存多份,所有群友都存储,消息冗余多;
  • 方案3:群聊消息存多份,只存ID,未利用偏序;
  • 终极方案:群聊消息存一份,只存last_ack_msgid。

架构不(只)是设计出来的,更是演进出来的。

(原文链接:点此进入,有改动)

即时通讯网 - 即时通讯开发者社区! 来源: - 即时通讯开发者社区!

上一篇:[已回复] MobileIMSDK iOS端更换域名后登录断线重连?下一篇:求教IM聊天好友功能-黑名单实现疑问

本帖已收录至以下技术专辑

推荐方案
评论 29
终级方案,看起来还挺诱人
签名: 星期六,那又怎样,还是得上班
这篇文章总结的很好,解决了我好久以来的各种混乱思路
签名: 立秋了,离凉爽的天气应该不远了吧
和好的方案, 感谢分享
很详细,作者辛苦了。
如果不是写扩散,那么,怎么才能做水平扩展,把用户分布到甚至是不同地区的服务器集群上?
引用:只是路过 发表于 2018-05-26 10:07
终级方案,看起来还挺诱人

我们现实方案与最后一种方案不谋而合
签名: 该会员没有填写今日想说内容.
引用:researchboy 发表于 2018-07-09 20:27
我们现实方案与最后一种方案不谋而合

嗯嗯
签名: 《正确理解IM长连接的心跳及重连机制,并动手实现》:http://www.52im.net/thread-2799-1-1.html
好厉害
神秘人  发表于 1 年前
有没有关于群聊太多,拉取的时候需要遍历这优化的处理方案呢?
引用:@_@ 发表于 2018-07-25 16:18
有没有关于群聊太多,拉取的时候需要遍历这优化的处理方案呢?

你把这里的文章都读一读:http://www.52im.net/forum.php?mo ... &ctid=20&fromop=all
签名: 《正确理解IM长连接的心跳及重连机制,并动手实现》:http://www.52im.net/thread-2799-1-1.html
写扩散的基础上,如何保证事务,会不会存在有的群成员消息写成功,有的群成员写失败的基础上,导致某些群成员丢消息的问题,不知道腾讯是怎么解决的,这么看来还是读扩散比较好
引用:zhxh007 发表于 2018-07-27 16:28
写扩散的基础上,如何保证事务,会不会存在有的群成员消息写成功,有的群成员写失败的基础上,导致某些群成 ...

个人理解和推想,写扩散的话,首先需要在群消息子系统中持久化消息和群里各用户的last_push_msgid,然后推送消息(写扩散)到用户消息子系统,如果推送成功则更新该用户的last_push_msgid,如果push失败则需要重试,直到消息推送到所有的用户。
个人猜测,微信采用写扩散的原因可能和业务模式有关,微信建群比较轻量,历史群组没有专门维护,不像QQ群。假设读扩散,那么每次拉取群消息时需要遍历所有的群,包括历史的,后台交互过多
引用:Fung 发表于 2018-09-30 18:12
个人猜测,微信采用写扩散的原因可能和业务模式有关,微信建群比较轻量,历史群组没有专门维护,不像QQ群。 ...

写扩散从技术实现上要比读扩散简单多了,微信团队公开的资料中说是使用的写扩散方案。写扩散唯一的缺点就是会增加存储的压力,或许微信很好地解决了存储的问题
签名: 《正确理解IM长连接的心跳及重连机制,并动手实现》:http://www.52im.net/thread-2799-1-1.html
存一份的想法是很好,但作者还是没有解答文章开头评论提出的疑问,类似微信这种无限建群的方式,如何知道哪些群有新消息?当用户群非常多的时候,可能只有某几个群有新消息。按需分页拉取的方案(先拉取群id, 再拉取消息内容)会很低效,意味着每次都要遍历所有群查看消息id差值。不知道这个问题有无解决方案。
引用:1mok 发表于 2019-01-28 15:58
存一份的想法是很好,但作者还是没有解答文章开头评论提出的疑问,类似微信这种无限建群的方式,如何知道哪 ...

我个人觉得解决方案是建立在实际用户场景里的,对于im而言主要纠结在实时推送【多端问题、及时性、数据不丢】,离线存储【主要是漫游、数据存储】。2种场景各有侧重点,实时推送注重消息的及时性,数据可靠不丢,客户端尽可能快速有效获取数据,所以解决思路是一般采用写扩散的模型,再加上ack应答的机制基本上可以解决。至于多端实时推送也认为只是一个数据推送纬度的问题。而无法成功推送的消息,为保证数据不丢一般采用离线存储,根据漫游和长时间离线的场景,此处考虑的是数据不丢,让用户能看到它想看到的数据才是第一要务。至于用户上线后,能瞬时同步海量消息的需求和百万在线弹幕系统一样,个人感觉都是伪需求,试想手机一登录,海量数据瞬时而来,手机就算不被卡死了,估计用户也看不到数据。这肯定是不对的。一般来说长时间不登录用户关心的是谁给我发过消息,发了几条消息。而不是具体的内容,在查看具体消息时,一般也是查看最新的若干条,同时要支持用户查询历史消息的功能,比如只显示最近100条数据,通过向上滚屏查看其它数据。至于作者说的数据偏向,只存一份数据,其实从设计上讲,每一条信息都应该我3个纬度的标识,一个是用户标识,一个是session标识,一个序列标识(或者是时间标识)。根据前面阐述,服务端需要维护不同会话最新消息的id,客户端需要维护已经同步消息的最新id,单调递增。实时推送时,用户id到端,sessionid到会话。但服务端注意推得量,最好有滑动窗口做适配或者降级方案。离线抽取数据时,首先获取最新的id计算出哪些人有消息,有多少消息。在根据操作动态抽取即可。这是当初我们设计【数据泵】时的思路,当时遇到难点数据显示顺序问题和id生成问题,推翻了很多设计,当时纠结了很久。
引用:1mok 发表于 2019-01-28 15:58
存一份的想法是很好,但作者还是没有解答文章开头评论提出的疑问,类似微信这种无限建群的方式,如何知道哪 ...

哪些群有新消息,服务端肯定是确切知道的,不需要客户端来按照这么多群一个个遍历拉取
签名: 《正确理解IM长连接的心跳及重连机制,并动手实现》:http://www.52im.net/thread-2799-1-1.html
引用:JackJiang 发表于 2019-02-27 21:36
哪些群有新消息,服务端肯定是确切知道的,不需要客户端来按照这么多群一个个遍历拉取

服务端肯定是可以计算出,但问题的关键是效率。一个人的会话数很多的时候(无限建群模式),如何快速找到存在未读消息的会话列表才是问题的核心。
引用:一夕 发表于 2019-02-27 20:53
我个人觉得解决方案是建立在实际用户场景里的,对于im而言主要纠结在实时推送【多端问题、及时性、数据不 ...

看的出来是有经验的人,你可以整理整理你的一些经验,分享出来。这方面的资料还是很稀缺的
签名: 《正确理解IM长连接的心跳及重连机制,并动手实现》:http://www.52im.net/thread-2799-1-1.html
打赏楼主 ×
使用微信打赏! 使用支付宝打赏!

返回顶部