微信为啥不丢 “离线消息”?

zzf2355 3年前
   <h2>需求缘起</h2>    <p>当发送方用户 A 发送消息给接收方用户 B 时, 如果用户 B 在线 ,之前的文章《 <a href="http://mp.weixin.qq.com/s?__biz=MjM5ODYxMDA5OQ==&mid=2651959606&idx=1&sn=f9561231dd33bcd0550b8d0d59d6b876&chksm=bd2d04ea8a5a8dfce90c870279a7f74b7aedd802c2d699dd919d7e40ebe30699381517c2d54b&scene=21#wechat_redirect" rel="nofollow,noindex">微信为啥不丢“在线消息”?</a> 》聊过,可以通过 应用层的确认,发送方的超时重传,接收方的去重保证业务层面消息的不丢不重 。</p>    <p>那如果接收方用户 B 不在线 ,系统是如何保证消息的可达性的呢?这是本文要讨论的问题。</p>    <p>问题:接收方不在线时,消息发送的流程是怎么样的?</p>    <p><img src="https://simg.open-open.com/show/9bf2f03906eb223274aba6655e4740ec.png"> 回答:如上图所述,</p>    <p>( 1 )用户 A 发送消息给用户 B</p>    <p>( 2 )服务器查看用户 B 的状态为 offline</p>    <p>( 3 )服务器将消息存储到 DB 中</p>    <p>( 4 )服务器返回用户 A 发送成功(对于发送方而言,消息落地 DB 就认为发送成功)</p>    <p>问题:离线消息表的设计,拉取离线的过程?</p>    <p>receiver_uid , msg_id, time, sender_uid ,msg_type, msg_content …</p>    <p>访问模式:接收方 B 要拉取发送方 A 给 ta 发送的离线消息,只需在 receiver_uid(B), sender_uid(A) 上查询,然后把离线消息删除,再把消息返回 B 即可。</p>    <p><img src="https://simg.open-open.com/show/6ef747c76869fd545ffffe970b26fa58.png"></p>    <p>整体流程如上图所述,</p>    <p>( 1 )用户 B 拉取用户 A 发送给 ta 的离线消息</p>    <p>( 2 )服务器从 DB 中拉取离线消息</p>    <p>( 3 )服务器从 DB 中把离线消息删除</p>    <p>( 4 )服务器返回给用户 B 想要的离线消息</p>    <p>问题:上述流程存在的问题?</p>    <p>回答: 如果用户 B 有很多好友 ,登陆时客户端需要对所有好友进行离线消息拉取, 客户端与服务器交互次数较多</p>    <p>客户端伪代码:</p>    <pre>  <code class="language-cpp">for(all uid in B’s friend-list){      // 登陆时所有好友都要拉取    get_offline_msg(B,uid);   // 与服务器交互    }</code></pre>    <p>优化方案一:先拉取各个好友的离线消息数量,真正 用户 B 进去看离线消息时,才往服务器发送拉取请求 (手机端为了节省流量,经常会使用这个 按需拉取 的优化)</p>    <p><img src="https://simg.open-open.com/show/e9bdca49661b54425908c8259445b795.jpg"></p>    <p>优化方案二:一次性拉取所有好友发送给用户 B 的离线消息, 到 <strong>客户端本地</strong> 再根据 sender_uid 进行计算 ,这样的话,离校消息表的访问模式就变为 -> 只需要按照 receiver_uid 来查询了。 登录时与服务器的交互次数降低为了 1 次 。</p>    <p>问题:用户 B <strong> 一次性拉取所有好友发给 ta </strong> 的离线消息,消息量很大时,一个请求包很大,速度慢,容易卡顿怎么办?</p>    <p><img src="https://simg.open-open.com/show/2c0f43672fd3fba497fb8e44ee5f32e3.jpg"></p>    <p>回答: 分页拉取 ,根据业务需求,先拉取最新(或者最旧)的一页消息,再按需一页页拉取。</p>    <p>问题:如何保证可达性,上述步骤第三步执行完毕之后,第四个步骤离线消息返回给客户端过程中,服务器挂点,路由器丢消息,或者客户端 crash 了,那离线消息岂不是丢了么(数据库已删除,用户还没收到)?</p>    <p>回答:嗯,如果按照上述的 1 , 2 , 3 , 4 步流程,的确是的,那 <strong>如何保证离线消息的可达性?</strong></p>    <p><img src="https://simg.open-open.com/show/99e2be5f69d0e02de00e8466614af024.png"></p>    <p>如同在线消息的应用层 ACK 机制一样,离线消息拉时,不能够直接删除数据库中的离线消息,而 必须等应用层的离线消息 ACK (说明用户 B 真的收到离线消息了),才能删除数据库中的离线消息 。</p>    <p>问题:如果用户 B <strong> 拉取了一页离线消息,却在 ACK </strong> <strong> 之前 crash </strong> <strong> 了,下次登录时会拉取到 重复的离线消息么 ? </strong></p>    <p>回答:拉取了离线消息却没有 ACK ,服务器不会删除之前的离线消息,故下次登录时系统层面还会拉取到。但在业务层面,可以根据 msg_id 去重。 SMC 理论: 系统层面无法做到消息不丢不重,业务层面可以做到,对用户无感知 。</p>    <p><img src="https://simg.open-open.com/show/fab0de1b23cdef683056cae31a5f4408.png"></p>    <p><strong>问题:假设有 N </strong> <strong> 页离线消息,现在每个离线消息需要一个 ACK </strong> ,那么岂不是 客户端与服务器的交互次数又加倍 了?有没有优化空间?</p>    <p><img src="https://simg.open-open.com/show/a662941a4ece3f9fb9f8c0e52cf17027.png"></p>    <p>回答: 不用每一页消息都 ACK ,在拉取第二页消息时相当于第一页消息的 ACK ,此时服务器再删除第一页的离线消息即可,最后一页消息再 ACK 一次 。这样的效果是,不管拉取多少页离线消息,只会多一个 ACK 请求,与服务器多一次交互。</p>    <h2>总结</h2>    <p>“离线消息”的可达性 可能比大家想象的要复杂,常见的优化有:</p>    <p>( 1 )对于同一个用户 B , 一次性拉取所有用户发给 ta 的离线消息,再在客户端本地进行发送方分析,相比按照发送方一个个进行消息拉取,能大大减少服务器交互次数</p>    <p>( 2 ) 分页拉取 ,先拉取计数再按需拉取,是无线端的常见优化</p>    <p>( 3 ) 应用层的 ACK ,应用层的去重 ,才能保证离线消息的不丢不重</p>    <p>( 4 ) 下一页的拉取,同时作为上一页的 ACK ,能够极大减少与服务器的交互次数</p>    <p> </p>    <p>来自:http://mp.weixin.qq.com/s?__biz=MjM5ODYxMDA5OQ==&mid=2651959629&idx=1&sn=053d85a862df6e6c01147a1cf95bdbf2&chksm=bd2d04918a5a8d87a32305fa8ef1603bb0e73e5c4b78afb5a144327841e08840188c4541b4aa&mpshare=1&scene=1&srcid=1103B6nv9Os1RRva4aqhGqvK#rd</p>    <p> </p>