Jack Jiang

我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
posts - 506, comments - 13, trackbacks - 0, articles - 1

本文引用了沈剑《如何保证IM实时消息的“时序性”与“一致性”?》一文的图片和内容(由于太懒,图没重新画),原文链接在文末。

1、引言

本文接上篇《零基础IM开发入门(三):什么是IM系统的可靠性?》,讲解IM系统中消息时序的一致性问题。

所谓的一致性,在IM中通常指的是消息时序的一致性,那就是:

  • 1)聊天消息的上下文连续性;
  • 2)聊天消息的绝对时间序。

再具体一点,IM消息的一致性体现在:

  • 1)单聊时:要保证发送方发出聊天消息的顺序与接收方看到的顺序一致;
  • 2)群聊时:要保证所有群员看到的聊天消息,与发送者发出消息时的绝对时间序是一致的。

IM系统中消息时序的一致性问题是个看似简单,实则非常有难度的技术热点话题之一,本文尽量以通俗简显的文字为你讲解IM消息时序一致性问题的产品意义、发生原因、解决思路等

  • 学习交流:

- 即时通讯/推送技术开发交流5群:215477170 [推荐]

- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM

- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK

(本文同步发布于:http://www.52im.net/thread-3189-1-1.html

2、系列文章

零基础IM开发入门(一):什么是IM系统?

零基础IM开发入门(二):什么是IM系统的实时性?

零基础IM开发入门(三):什么是IM系统的可靠性?

零基础IM开发入门(四):什么是IM系统的消息时序一致性?》(* 本文

《零基础IM开发入门(五):什么是IM系统的安全性? (稍后发布)》

《零基础IM开发入门(六):什么是IM系统的的心跳机制? (稍后发布)》

《零基础IM开发入门(七):如何理解并实现IM系统消息未读数? (稍后发布)》

《零基础IM开发入门(八):如何理解并实现IM系统的多端消息漫游? (稍后发布)》

3、消息时序的一致性,对于IM的意义

现如今,由于移动互联网的普及,现代人的实际社交关系,几乎完全是靠IM这种即时通讯社交工具所组织起来的,IM这种工具的重要性不言而喻。

IM在现代人的生活中,越来越重要,但也越来平常。现在想联系一个人,第一时间想到的不是打个电话,而是发个“微信”或“QQ”。是的,IM这玩意承载的意义越来越多。

消息时序的一致性问题,对于IM的意义,毫无疑问带来的不只是简简单单的所谓用户体验问题。我们来看看例子。

假设:你跟女神的表白正进入到关键阶段,聊着聊着就因为这烂IM,导致聊天消息前言不搭后语,此刻1000公里外你的女神真一脸蒙逼的盯着手机看你的“醉话”,后果是多么严重——这半年来的“舔狗”生活、忍辱负重,算是白白被程序员这群“格子衫”、“地中海”们给祸害了。

再往严重了说,就因为这烂IM,让你失去了这难得的借助女神优良基因,改造家族后代颜值的绝佳机会,无不让人痛心疾首。。。

上面这个例子,说的还只是单聊,如果是群聊,则问题可能还会被无限放大:试想一个技术交流群,正在激烈的争吵着“php是世界最好语言”这种话题的时候,忽然就想砸手机了,不是因为吵的太凶,而因为消息顺序全乱完全没法看,已经严重影响键盘侠们积极发表个人意见了。

4、凭什么说保证消息时序的一致性很困难?

4.1 基本认知

在普通IM用户的眼里,消息无非是从一台手机传递到另一台手机而已,保证时序有何困难?

是的,普通用户这么认为,从技术上讲,他只是单纯的将IM消息的收发过程理解为单线程的工作模式而已。

实际上,在IM这种高性能场景下,服务端为了追求高吞吐、高并发,用到了多线程、异步IO等等技术。

在这种情况下,“高并发”与“顺序”对于IM服务端来说,本来就是矛盾的,这就有点鱼与熊掌的味道了(两者很难兼得)。

所以,要实现IM场景下的消息时序一致性,需要做出权衡,而且要考虑的技术维度相当多。这就导致具体技术实施起来没有固定的套路,而由于开发者技术能力的参差不齐,也就使得很多IM系统在实际的效果上会有较大问题,对于用户而言也将直接在产品体验上反应出来。

下面将具体说明技术难点所在。

4.2 没有全局时钟

如上图所示,一个真正堪用的生产系统,显示不可能所有服务都跑在一台服务器上,分布式环境是肯定的。

那么:在分布式环境下,客户端+服务端后台的各种后台服务,都各自分布在不同的机器上,机器之间都是使用的本地时钟,没有一个所谓的“全局时钟”(也没办法做到真正的全局时钟),那么所谓的消息时序也就没有真正意义上的时序基准点。所以消息时序问题显然不是“本地时间”可以完全决定的。

4.3 多发送方问题

服务端分布式的情况下,不能用“本地时间”来保证时序性,那么能否用接收方本地时间表示时序呢?

遗憾的是,由于多个客户端的存在(比如群聊时),即使是一台服务器的本地时间,也无法表示“绝对时序”。

如上图所示:绝对时序上,APP1先发出msg1,APP2后发出msg2,都发往服务器web1,网络传输是不能保证msg1一定先于msg2到达的,所以即使以一台服务器web1的时间为准,也不能精准描述msg1与msg2的绝对时序。

4.4 多接收方问题

多发送方不能保证时序,假设只有一个发送方,能否用发送方的本地时间表示时序呢?遗憾的是,由于多个接收方的存在,无法用发送方的本地时间,表示“绝对时序”。

如上图,绝对时序上,web1先发出msg1,后发出msg2,由于网络传输及多接收方的存在,无法保证msg1先被接收到先被处理,故也无法保证msg1与msg2的处理时序。

4.5 网络传输与多线程问题

既然多发送方与多接收方都难以保证绝对时序,那么假设只有单一的发送方与单一的接收方,能否保证消息的绝对时序一致性呢?

结论是悲观的,由于网络传输与多线程的存在,这仍然不行。

如上图所示,web1先发出msg1、后发出msg2,即使msg1先到达(网络传输其实还不能保证msg1先到达),由于多线程的存在,也不能保证msg1先被处理完。

5、如何保证绝对的消息时序一致性?

通过上一章内容的总结,我们已经对IM中消息时序一致性问题所产生的缘由,有了较为深刻的认识。

从纯技术的角度来说,假设:

  • 1)只有一个发送方;
  • 2)一个接收方;
  • 3)上下游连接只有一条socket连接;
  • 4)通过阻塞的方式通讯。

这样的情况下,难道不能保证先发出的消息被先处理,进而被先展示给消息的接收者吗?

是的,可以!

但实际生产情况下不太可能出现这种IM系统,必竟单发送方、单接收方、单socket连接、阻塞方式,这样的IM一旦做出来,产品经理会立马死给你看。。。

6、实用的优化思路

6.1 一对一单聊的消息一致性保证思路

假设两人一对一聊天,发送方A依次发出了msg1、msg2、msg3三条消息给接收方B,这三条消息该怎么保证显示时序的一致性(发送与显示的顺序一致)?

我们知道,发送方A依次发出的msg1、msg2、msg3三条消息,到底服务端后,再由服务端中转发出时,这个顺序由于多线程的网络的问题,是有可能乱序的。

那么结果就可能是这样: 

如上图所示,会出现与发出时的消息时序不一致问题(收到的消息顺序是:msg3、msg1、msg2)。

不过,实际上一对一聊天的两个人,并不需要全局消息时序的一致(因为聊天只在两人的同一会话在发生),只需要对于同一个发送方A,发给B的消息时序一致就行了。

常见优化方案,在A往B发出的消息中,加上发送方A本地的一个绝对时序(比如本机时间戳),来表示接收方B的展现时序。

那么当接收方B收到消息后,即使极端情况下消息可能存在乱序到达,但因为这个乱序的时间差对于普通用户来说体感是很短的,在UI展现层按照消息中自带的绝对时序排个序后再显示,用户其实是没有太多感知的。

6.2 多对多群聊的消息一致性保证思路

假设N个群友在一个IM群里聊天,应该怎样保证所有群员收到消息的显示时序一致性呢?

首先:不能像一对聊天那样利用发送方的绝对时序来保证消息顺序,因为群聊发送方不单点,时间也不一致。

或许:我们可以利用服务器的单点做序列化。

如上图所示,此时IM群聊的发送流程为:

  • 1)sender1发出msg1,sender2发出msg2;
  • 2)msg1和msg2经过接入集群,服务集群;
  • 3)service层到底层拿一个唯一seq,来确定接收方展示时序;
  • 4)service拿到msg2的seq是20,msg1的seq是30;
  • 5)通过投递服务讲消息给多个群友,群友即使接收到msg1和msg2的时间不同,但可以统一按照seq来展现。

这个方法:

  • 1)优点是:能实现所有群友的消息展示时序相同;
  • 2)缺点是:这个生成全局递增序列号的服务很容易成为系统瓶颈。

还有没有进一步的优化方法呢?

从技术角度看:群消息其实也不用保证全局消息序列有序,而只要保证一个群内的消息有序即可,这样的话,“消息id序列化”就成了一个很好的思路。

上图这个方案中,service层不再需要去一个统一的后端拿全局seq(序列号),而是在service连接池层面做细小的改造,保证一个群的消息落在同一个service上,这个service就可以用本地seq来序列化同一个群的所有消息,保证所有群友看到消息的时序是相同的。

关于IM的系统架构下使用怎么样实现消息序列化,或者说全局消息ID的生成方案,这又是另一个很热门的技术话题。

有兴趣,可以深入阅读下面这个系列:

IM消息ID技术专题(一):微信的海量IM聊天消息序列号生成实践(算法原理篇)

IM消息ID技术专题(二):微信的海量IM聊天消息序列号生成实践(容灾方案篇)

IM消息ID技术专题(三):解密融云IM产品的聊天消息ID生成策略

IM消息ID技术专题(四):深度解密美团的分布式ID生成算法

IM消息ID技术专题(五):开源分布式ID生成器UidGenerator的技术实现

IM消息ID技术专题(六):深度解密滴滴的高性能ID生成器(Tinyid)

这个系列中,尤其微信的趋势递增ID生成思路(注意:趋势递增不是严格递增,趋势递增意味着中问有ID被跳过也没事),对于分布式IM的消息ID来说是非常切实可行的。

是的,对于IM系统来说,绝对意义上的时序很难保证,但通过服务端生成的单调递增消息ID的方式,利用递增ID来保证时序性,也是一个很可性的方案。

7、小结一下

IM系统架构下,消息的绝对时序是很困难的,原因多种多样,比如:没有全局时钟、多发送方、多接收方、多线程、网络传输不确定性等。

一对一单聊时,其实只需要保证发出的时序与接收的时序一致,就基本能让用户感觉不到乱序了。

多对多的群聊情况下,保证同一群内的所有接收方消息时序一致,也就能让用户感觉不到乱序了,方法有两种,一种单点绝对时序,另一种实现消息id的序列化(也就是实现一种全局递增消息ID)。

8、参考资料

[1] 如何保证IM实时消息的“时序性”与“一致性”?,作者:沈剑

[2] 一个低成本确保IM消息时序的方法探讨,作者:封宇

附录:更多IM开发热门技术点

移动端IM开发者必读(一):通俗易懂,理解移动网络的“弱”和“慢”

移动端IM开发者必读(二):史上最全移动弱网络优化方法总结

现代移动端网络短连接的优化手段总结:请求速度、弱网适应、安全保障

移动端IM中大规模群消息的推送如何保证效率、实时性?

移动端IM开发需要面对的技术问题

开发IM是自己设计协议用字节流好还是字符流好?

请问有人知道语音留言聊天的主流实现方式吗?

如何保证IM实时消息的“时序性”与“一致性”?

一个低成本确保IM消息时序的方法探讨

IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?

IM群聊消息如此复杂,如何保证不丢不重?

谈谈移动端 IM 开发中登录请求的优化

移动端IM登录时拉取数据如何作到省流量?

浅谈移动端IM的多点登录和消息漫游原理

完全自已开发的IM该如何设计“失败重试”机制?

微信对网络影响的技术试验及分析(论文全文)

IM开发基础知识补课(五):通俗易懂,正确理解并用好MQ消息队列

微信技术分享:微信的海量IM聊天消息序列号生成实践(算法原理篇)

IM开发基础知识补课(六):数据库用NoSQL还是SQL?读这篇就够了!

IM里“附近的人”功能实现原理是什么?如何高效率地实现它?

IM的扫码登录功能如何实现?一文搞懂主流应用的扫码登录技术原理

IM开发宝典:史上最全,微信各种功能参数和逻辑规则资料汇总

本文已同步发布于“即时通讯技术圈”公众号。

▲ 本文在公众号上的链接是:点此进入,原文链接是:http://www.52im.net/thread-3189-1-1.html

posted @ 2020-11-04 14:31 Jack Jiang 阅读(189) | 评论 (0)编辑 收藏

本文编写时引用了“聊聊IM系统的即时性和可靠性”一文的部分内容和图片,感谢原作者。

1、引言

上一篇《零基础IM开发入门(二):什么是IM系统的实时性?》讲到了IM系统的“立足”之本——“实时性”这个技术特征,本篇主要讲解IM系统中的“可靠性”这个话题,内容尽量做到只讲原理不深入展开,避开深层次的技术性探讨,确保通俗易懂。


阅读对象:本系列文章主要阅读对象为零IM基础的开发者或产品经理,目标是告诉你“IM系统是什么?”,尽量不深入探讨具体的技术实现,确保通俗易懂,老少皆宜。

如您想从技术维度系统学习IM技术并着手自已的IM开发(即解决“IM系统要怎么做?”这个疑问),请从此文开始:《新手入门一篇就够:从零开发移动端IM》。

学习交流:

- 即时通讯/推送技术开发交流5群:215477170[推荐]

- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM

- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK

(本文同步发布于:http://www.52im.net/thread-3182-1-1.html

2、系列文章

零基础IM开发入门(一):什么是IM系统?

零基础IM开发入门(二):什么是IM系统的实时性?

零基础IM开发入门(三):什么是IM系统的可靠性?》(* 本文

零基础IM开发入门(四):什么是IM系统的消息时序一致性?

《零基础IM开发入门(五):什么是IM系统的安全性? (稍后发布)》

《零基础IM开发入门(六):什么是IM系统的的心跳机制? (稍后发布)》

《零基础IM开发入门(七):如何理解并实现IM系统消息未读数? (稍后发布)》

《零基础IM开发入门(八):如何理解并实现IM系统的多端消息漫游? (稍后发布)》

3、正文概述

一般来说,IM系统的消息“可靠性”,通常就是指聊天消息投递的可靠性(准确的说,这个“消息”是广义的,因为还存用户看不见的各种指令,为了通俗,统称“消息”)。

从用户行为来讲,消息“可靠性”应该分为两种类型:

  • 1)在线消息的可靠性:即发送消息时,接收方当前处于“在线”状态;
  • 2)离线消息的可靠性:即发送消息时,接收方当前处于“离线”状态。

从具体的技术表现来讲,消息“可靠性”包含两层含义:

  • 1)消息不丢:这很直白,发出去的消息不能像进了黑洞一样,一脸懵逼可不行;
  • 2)消息不重:这是丢消息的反面,消息重复了也不能容忍。

对于“消息不丢”这个特征来说,细化下来,它又包含两重含义:

  • 1)已明确被对方收到;
  • 2)已明确未被对方收到。

是的,对于第1)重含义好理解,第2)重含义的意思是:当对方没有成功收到时,你的im系统也必须要感知到,否则,它同样属于被“丢”范畴。

总之,一个成型的im系统,必须包含这两种消息“可靠性”逻辑,才能堪用,缺一不可。

消息的可靠性(不丢失、不重复)无疑是IM系统的重要指标,也是IM系统实现中的难点之一。本文以下文字,将从在线消息的可靠性和离线消息的可靠性进行讨论。

4、典型的在线消息收发流程

先看下面这张典型的im消息收发流程: 

是的,这是一个典型的服务端中转型IM架构。

所谓“服务端中转型IM架构”是指:一条消息从客户端A发出后,需要先经过 IM 服务器来进行中转,然后再由 IM 服务器推送给客户端B,这种模式也是目前最常见的 IM 系统的消息分发架构。

你可能会说,IM不可以是P2P模式的吗?是的,目前来说主流IM基本都是服务器中转这种方式,P2P模式在IM系统中用的很少。

原因是以下两个很明显的弊端:

  • 1)P2P模式下,IM运营者很容易被用户架空(无法监管到用户行为,用户涉黄了怕不怕?);
  • 2)P2P模式下,群聊这种业务形态,很难实现(我要在千人群中发消息给,不可能我自已来分发1000次吧)。

话题有点跑偏,我们回到正题:在上面这张图里,客户A发送消息到服务端、服务端中转消息给客户B,假设这两条数据链接中使用的通信协议是TCP,你认为在TCP所谓可靠传输协议加持下,真的能保证IM聊天消息的可靠性吗?

答案是否定的。我们继续看下节。

5、TCP并不能保证在线消息的“可靠性”

接上节,在一个典型的服务端中转型IM架构中,即使使用“可靠的传输协议”TCP,也不能保证聊天消息的可靠性。为什么这么说?

要回答这个问题,网上的很多文章,都会从服务端的角度举例:比如消息发送时操作系统崩溃、网络闪断、存储故障等等,总之很抽象,不太容易理解。

这次我们从客户端角度来理解,为什么使用了可靠传输协议TCP的情况下IM聊天消息仍然不可靠的问题。

具体来说:如何确保 IM 消息的可靠性是个相对复杂的话题,从客户端发送数据到服务器,再从服务器送达目标客户端,最终在 UI 成功展示,其间涉及的环节很多,这里只取其中一环「接收端如何确保消息不丢失」来探讨,粗略聊下我接触过的两种设计思路。

说到可靠送达:第一反应会联想到 TCP 的可靠性。数据的可靠送达是个通用性的问题,无论是网络二进制流数据,还是上层的业务数据,都有可靠性保障问题,TCP 作为网络基础设施协议,其可靠性设计的可靠性是毋庸置疑的,我们就从 TCP 的可靠性说起。

在 TCP 这一层:所有 Sender 发送的数据,每一个 byte 都有标号(Sequence Number),每个 byte 在抵达接收端之后都会被接收端返回一个确认信息(Ack Number), 二者关系为 Ack = Seq + 1。简单来说,如果 Sender 发送一个 Seq = 1,长度为 100 bytes 的包,那么 receiver 会返回一个 Ack = 101 的包,如果 Sender 收到了这个Ack 包,说明数据确实被 Receiver 收到了,否则 Sender 会采取某种策略重发上面的包。

第一个问题是:既然 TCP 本身是具备可靠性的,为什么还会出现消息接收端(Receiver)丢失消息的情况?

看下图一目了然:

▲ 上图引用自《从客户端的角度来谈谈移动端IM的消息可靠性和送达机制

一句话总结上图的含义:网络层的可靠性不等同于业务层的可靠性。

数据可靠抵达网络层之后,还需要一层层往上移交处理,可能的处理有:

  • 1)安全性校验;
  • 2)binary 解析;
  • 3)model 创建;
  • 4)写 db;
  • 5)存入 cache;
  • 6)UI 展示;
  • 7)以及一些边界问题:比如断网、用户突然退出登陆、磁盘已满、内存溢出、app奔溃、突然关机等等。

项目的功能特性越多,网络层往上的处理出错的可能性就越大。

举个最简单的场景为例子:消息可靠抵达网络层之后,写 db 之前 IM APP 崩溃(不稀奇,是 App 都有崩溃的可能),虽然数据在网络层可靠抵达了,但没存进 db,下次用户打开 App 消息自然就丢失了,如果不在业务层再增加可靠性保障(比如:后面要提到的网络层面的消息重发保障),那么意味着这条消息对于接收端来说就永远丢失了,也就自然不存在“可靠性”了。

从客户端角度理解IM的可能性以及解决办法,可以详细阅读:从客户端的角度来谈谈移动端IM的消息可靠性和送达机制》,本节引用的是该文中“4、TCP协议的可靠性之外还会出现消息丢失?”一节的文字。

6、为在线消息增加“可靠性”保障

那么怎样在应用层增加可靠性保障呢?

有一个现成的机制可供我们借鉴:TCP协议的超时、重传、确认机制。

具体来说就是:

  • 1)在应用层构造一种ACK消息,当接收方正确处理完消息后,向发送方发送ACK;
  • 2)假如发送方在超时时间内没有收到ACK,则认为消息发送失败,需要进行重传或其他处理。

增加了确认机制的消息收发过程如下: 

我们可以把整个过程分为两个阶段。

阶段1:clientA -> server

  • 1-1:clientA向server发送消息(msg-Req);
  • 1-2:server收取消息,回复ACK(msg-Ack)给clientA;
  • 1-3:一旦clientA收到ACK即可认为消息已成功投递,第一阶段结束。

无论msg-A或ack-A丢失,clientA均无法在超时时间内收到ACK,此时可以提示用户发送失败,手动进行重发。

阶段2:server -> clientB

  • 2-1:server向clientB发送消息(Notify-Req);
  • 2-2:clientB收取消息,回复ACK(Notify-ACk)给server;
  • 2-3:server收到ACK之后将该消息标记为已发送,第二阶段结束。

无论msg-B或ack-B丢失,server均无法在超时时间内收到ACK,此时需要重发msg-B,直到clientB返回ACK为止。

关于IM聊天消息的可靠性保障问的深入讨论,可以详读:IM消息送达保证机制实现(一):保证在线实时消息的可靠投递》,该文会深入讨论这个话题。

7、典型的离线消息收发流程

说完在线消息的“可靠性”问题,我们该了解一下离线消息了。

7.1 离线消息的收发也存在“不可靠性”

下图是一张典型的IM离线消息流程图:

如上图所示,和在线消息收发流程类似。

离线消息收发流程也可划分为两个阶段:

阶段1:clientA -> server

  • 1-1:clientA向server发送消息(msg-Req) ;
  • 1-2:server发现clientB离线,将消息存入offline-DB。

阶段2:server -> clientB

  • 2-1:clientB上线后向server拉取离线消息(pull-Req) ;
  • 2-2:server从offline-DB检索相应的离线消息推送给clientB(pull-res),并从offline-DB中删除。

显然:离线消息收发过程同样存在消息丢失的可能性。

举例来说:假设pull-res没有成功送达clientB,而offline-DB中已删除,这部分离线消息就彻底丢失了。

7.2 离线消息的“可靠性”保障

与在线消息收发流程类似,我们同样需要在应用层增加可靠性保障机制。

下图是增加了可靠性保障后的离线消息收发流程: 

与初始的离线消息收发流程相比,上图增加了1-3、2-4、2-5步骤:

  • 1-3:server将消息存入offline-DB后,回复ACK(msg-Ack)给clientA,clientA收到ACK即可认为消息投递成功;
  • 2-4:clientB收到推送的离线消息,回复ACK(res-Ack)给server;
  • 2-5:server收到res-ACk后确定离线消息已被clientB成功收取,此时才能从offline-DB中删除。

当然,上述的保障机制,还存在性能优化空间。

当离线消息的量较大时:如果对每条消息都回复ACK,无疑会大大增加客户端与服务器的通信次数。这种情况我们通常使用批量ACK的方式,对多条消息仅回复一个ACK。在某此后IM的实现中是将所有的离线消息按会话进行分组,每组回复一个ACK,假如某个ACK丢失,则只需要重传该会话的所有离线消息。

有关离线消息的可靠性保障机制的详细讨论,可以详读:IM消息送达保证机制实现(二):保证离线消息的可靠投递》、《IM开发干货分享:如何优雅的实现大量离线消息的可靠投递》,这两篇文章可以给你更深入具体的答案。

8、聊天消息重复的问题

上面章节中,通过在应用层加入重传、确认机制后,我们确实是杜绝了消息丢失的可能性。

但由于重试机制的存在,我们会遇到一个新的问题:那就是同一条消息可能被重复发送。

举一个最简单的例子:假设client成功收到了server推送的消息,但其后续发送的ACK丢失了,那么server将会在超时后再次推送该消息,如果业务层不对重复消息进行处理,那么用户就会看到两条完全一样的消息。

消息去重的方式其实非常简单,一般是根据消息的唯一标志(id)进行过滤。

具体过程在服务端和客户端可能有所不同:

  • 1)客户端 :我们可以通过构造一个map来维护已接收消息的id,当收到id重复的消息时直接丢弃;
  • 2)服务端 :收到消息时根据id去数据库查询,若库中已存在则不进行处理,但仍然需要向客户端回复Ack(因为这条消息很可能来自用户的手动重发)。

关于消息的去重问题,在一对一聊天的情况下,逻辑并不复杂,但在群聊模式下,会将问题复杂化,有关群聊消息不丢和去重的详细讨论,可以深入阅读:《IM群聊消息如此复杂,如何保证不丢不重?》。

9、本文小结

保证消息的可靠性是IM系统设计中很重要的一环,能不能做到“消息不丢”、“消息不重”,对用户的体验影响极大。

所谓“可靠的传输协议”TCP也并不能保障消息在应用层的可靠性。

我们一般通过在应用层的ACK应答和重传机制,来实现IM消息的可靠性保障。但由此带来的消息重复问题,需要我们额外进行处理,最简单的方法就是通过消息ID进行幂等去重。

关于IM系统消息可靠性的理论基础,我们就探讨到这里,有疑问的读者,可以在本文末尾留意,欢迎积极讨论。

10、参考资料

[1] IM消息送达保证机制实现(一):保证在线实时消息的可靠投递

[2] IM消息送达保证机制实现(二):保证离线消息的可靠投递

[3] IM开发干货分享:如何优雅的实现大量离线消息的可靠投递

[4] 从客户端的角度来谈谈移动端IM的消息可靠性和送达机制

[5] 聊聊IM系统的即时性和可靠性

[6] 学习笔记4——IM系统如何保证消息的可靠性

[7] IM群聊消息如此复杂,如何保证不丢不重?

附录:更多IM开发热门技术点

移动端IM开发者必读(一):通俗易懂,理解移动网络的“弱”和“慢”

移动端IM开发者必读(二):史上最全移动弱网络优化方法总结

现代移动端网络短连接的优化手段总结:请求速度、弱网适应、安全保障

移动端IM中大规模群消息的推送如何保证效率、实时性?

移动端IM开发需要面对的技术问题

开发IM是自己设计协议用字节流好还是字符流好?

请问有人知道语音留言聊天的主流实现方式吗?

如何保证IM实时消息的“时序性”与“一致性”?

一个低成本确保IM消息时序的方法探讨

IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?

IM群聊消息如此复杂,如何保证不丢不重?

谈谈移动端 IM 开发中登录请求的优化

移动端IM登录时拉取数据如何作到省流量?

浅谈移动端IM的多点登录和消息漫游原理

完全自已开发的IM该如何设计“失败重试”机制?

微信对网络影响的技术试验及分析(论文全文)

IM开发基础知识补课(五):通俗易懂,正确理解并用好MQ消息队列

微信技术分享:微信的海量IM聊天消息序列号生成实践(算法原理篇)

IM开发基础知识补课(六):数据库用NoSQL还是SQL?读这篇就够了!

IM里“附近的人”功能实现原理是什么?如何高效率地实现它?

IM的扫码登录功能如何实现?一文搞懂主流应用的扫码登录技术原理

IM消息ID技术专题(一):微信的海量IM聊天消息序列号生成实践(算法原理篇)

IM消息ID技术专题(二):微信的海量IM聊天消息序列号生成实践(容灾方案篇)

IM消息ID技术专题(三):解密融云IM产品的聊天消息ID生成策略

IM消息ID技术专题(四):深度解密美团的分布式ID生成算法

IM消息ID技术专题(五):开源分布式ID生成器UidGenerator的技术实现

IM消息ID技术专题(六):深度解密滴滴的高性能ID生成器(Tinyid)

IM开发宝典:史上最全,微信各种功能参数和逻辑规则资料汇总

本文已同步发布于“即时通讯技术圈”公众号。

▲ 本文在公众号上的链接是:点此进入,原文链接是:http://www.52im.net/thread-3182-1-1.html

posted @ 2020-10-29 14:09 Jack Jiang 阅读(181) | 评论 (0)编辑 收藏

     摘要: 本文原题“Node.js - 200 多行代码实现 Websocket 协议”,为了提升内容品质,有较大修订。1、引言最近正在研究 WebSocket 相关的知识,想着如何能自己实现 WebSocket 协议。到网上搜罗了一番资料后用 Node.js 实现了一个WebSocket协议服务器,倒也没有想象中那么复杂,除去注释语句和 console 语句后,大约 200 行代码...  阅读全文

posted @ 2020-10-21 14:41 Jack Jiang 阅读(279) | 评论 (0)编辑 收藏

     摘要: 本文原题“WebSocket:5分钟从入门到精通”,作者“程序猿小卡_casper”,原文链接见文末参考资料部分。本次收录时有改动。1、引言自从HTML5里的WebSocket出现后,彻底改变了以往Web端即时通讯技术的基础通道这个“痛点”(在此之前,开发者们不得不弄出了诸如:短轮询、长轮询、Comet、SSE等技术,可谓苦之...  阅读全文

posted @ 2020-10-14 14:40 Jack Jiang 阅读(387) | 评论 (0)编辑 收藏

     摘要: 本文由融云技术团队原创投稿,作者是融云WebRTC高级工程师苏道,转载请注明出处。1、引言在一个典型的IM应用里,使用实时音视频聊天功能时,视频首帧的显示,是一项很重要的用户体验指标。本文主要通过对WebRTC接收端的音视频处理过程分析,来了解和优化视频首帧的显示时间,并进行了总结和分享。(本文同步发布于:http://www.52im.net/thread-3169-1-1.html)2、什么是...  阅读全文

posted @ 2020-09-28 22:17 Jack Jiang 阅读(268) | 评论 (0)编辑 收藏

本文引用自“蚂蚁金服科技”公众号,原文由支付宝技术团队原创分享。 本次收录时有改动。

1、引言

最早接触2维码扫码功能,是在2011年,那会移动互联网正是起步阶段,大家都感觉智能手机可以更强大,但到底要做些什么功能,都是在探索和创新。2维码扫描功能就是这些创新功能之一。

当然,2维码扫描说到底还是图像识别,这种技术不是一般的公司能搞定的,所以大家用的最多的2维码扫描库就是ZXing。这个库,很多人应该非常熟悉,用过这个库的人,基本都记住了下面这个图片(ZXing的logo)。

▲ ZXing工程的logo

这个库的使用前题就是需要手机摄像头有自动对焦功能,那会手机成本还没现在这么低,所以自动对焦功能不是所有手机都具备,也就限制了2维码扫码功能在一些较低端的手机上的使用,同时也制约了扫码功能的普及。

后来的事情大家都知道了,微信这种社交IM越来越受欢迎,微信里可以用来扫码加好友的“扫一扫”功能,让2维码扫描功能几乎变成了IM软件的标配。

 

▲ 早期微信里的“扫一扫”功能

现在的微信,不仅可以扫2维友加好友,还可以扫码支付以及各种图像识别方面的功能,越做越丰富。2维码扫码功能从一个单一的图像识别技术,逐渐演变成了移动线联网的入口功能。

从去年开始,微信对2维码扫描功能进行了升级,不仅可以在UI上让用户知道被扫描2维码的中心点,还能同时识别最多3个2维码,相当强大。

 

▲ 现在的微信可以同时识别最多3个2维码(注意绿点)

如上图所示,原本大家都习以为常的2维码扫描功能,原来还可以做的这么友好。

正好看到支付宝分享的这篇2维码扫描优化文章,市面上真正分享这方面的技术的文章几乎没有,而2维码扫码加友作为IM中最常见的功能之一,对于即时通讯网的开发者来说,虽然不需要自已从底层开发,但了解这方面的知识,还是很有必要的。

本文要分享的是支付宝针对2维码扫描功能,在2维码残缺、变形、变色等等恶劣条件下,是如何提升扫码识别率、识别速度的技术实践总结。希望能带给你启发。

学习交流:

- 即时通讯/推送技术开发交流5群:215477170[推荐]

- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM

- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK

(本文同步发布于:http://www.52im.net/thread-3150-1-1.html

2、技术背景

随着支付宝的线下场景不断扩大,收钱码、口碑、共享单车、充电宝、停车缴费等产品让我们的生活越来越便利。

二维码因为成本低、兼容性好成为了线上线上最主要的连接工具,也因此面临更多新的挑战。

因为二维码是一种点阵式信息编码方式,任何视觉上的缺损、弯曲以及光线作用都会极大的影响识别成功率,如果识别困难也就意味着用户可能选择放弃,影响支付体验也影响用户心智。

用户扫码体验的最关键的主要有以下几个因素:

  • 1)识别率:这是扫码服务的基础指标,识别率能直接体现识别能力,识别率如果无法提高意味着大量的用户将无法使用更便捷的服务;
  • 2)识别耗时:包括 app 启动耗时以及图像识别耗时,这是衡量一个用户从点击 app 到正确识别到内容耗时,每增加 1s,将有相当大量的用户放弃等待并离开;
  • 3)精准反馈:识别结果不仅需要及时反馈给用户,还需要非常精准,特别是在目前线下有多个二维码的场景下,需要避免用户二次操作。

本文将从以上三个方面,分享支付宝扫码技术团队是如何为用户打造一个又准又快又稳的极致扫码体验。

我们对用户反馈进行了大量统计分析,发现绝大部分识别失败都是因为二维码并不标准,并且很遗憾的是在使用我们早期的扫码版本进行识别率测试时发现识别率只有 60%。下面的文字,将首先从提高识别率的方向着手。

3、提高识别率策略1:优化桩点查找算法长宽比耐受

以往的扫码算法,检查长宽比例时允许差异 40%,但是由于使用前向误差,判断结果跟长宽的先后顺序相关,这会导致有些长宽比失调的码,横着扫不出来,但是旋转 90 度竖着却能扫出来了(^OMG^)。

优化策略总结:

  • 1)通过修改长宽比的判定规则,长宽比将不再受先后顺序影响;
  • 2)对于已知长度,修改规则将可接受的宽度范围扩大,增强长宽比的耐受。

在我们对比测试集中,识别率提高了 1% 左右。

4、提高识别率策略2:新增1:5:1桩点识别模式模式

在一张图片中,要找到二维码,关键在找二维码特征定位点: 

三个角的回字型图案,这就是二维码特征定位点。

中间区域的黑白色块比例是1:1:3:1:1:

以往的扫码算法,桩点识别是通过状态机 查找11311模式后 取中间位置确定x位置(此时扫描线在第一行11311比例处)在x位置纵向搜索11311模式, 确定y位置再以 (x,y) 位置横向搜索11311比例,修正x位置。

这种模式在桩点污损的情况下,识别能力较差只要在任何一次11311模式搜索中遇到干扰点,哪怕是一个像素的椒盐噪声也能使桩点查找失败。(支付宝蓝的桩点,会在蓝色区域产生大量噪点,导致识别率低下)

为此,我们新增了一种桩点识别方式。在状态机达到151模式的时候,开始尝试确认桩点。(此时扫描线在第一行151比例处)。

优化效果:

  • 1)新的查找方法将不再受桩点中心或边缘部分被污损的影响,支付宝蓝色桩点码识别率明显提升;
  • 2)修改后识别率整体提升了接近 1%,但识别失败的耗时有所提升。

5、提高识别率策略3:添加一种对角线过滤规则

在枚举所有可能桩点组合 O(N^3) 之前,对所有可疑桩点进行一次对角线检查过滤。由于桩点对角线也应该满足 11311模式 ,用这个规则做一次过滤可疑有效减少运算量,也就有效降低了识别成功和失败的耗时。

6、提高识别率策略4:基于 Logistic Regression 的二维码分类器

在以往的扫码算法中在拿到三个桩点后,基于夹角,长度偏差,单位长度查三个数值,用简单公式计算得到阈值,判断是否为可能的二维码,误判概率较大。

为此,我们引入机器学习中的逻辑回归算法模型。

基于支付宝丰富的二维码数据集,训练出逻辑回归模型,作为二维码分类器,明显降低了误判概率,也将明显降低无二维码时识别失败的耗时。

7、提高识别率策略5:修改跳行扫描的间隔数

由于输入的相机帧分辨率高,像素点多,运算量大,以往的扫码算法在水平跟垂直方向跳行采样进行计算。但在实际运算中,由于跳过了太多列,错过了11311模式中某些1位置的点,导致桩点查找失败。

我们通过将跳行计算行数修改为可配置项,通过线上 AB 灰度测试得到最合适的跳行策略,整体配置此跳行策略后,识别率得到明显提升。

上述优化在测试集的表现: 

综上优化:扫码核心识别能力,在7744张图片测试集上提高了6.95个百分点。

8、特殊策略优化

除此上述通用扫码优化之外,我们还对特殊场景扫码能力进行提高。

8.1 畸变?不怕不怕!

线下场景复杂多变。饮料瓶身上变形的二维码、超市小票卷起边角弯曲的二维码、路边小贩凹凸不平甚至折叠的二维码......这些畸变的二维码容易增加识别难度,甚至导致识别失败。

以往的扫码算法抗畸变策略中,先用透视变换关系建立映射关系。

优点是:适应性好,满足大多数应用场景。

不足也明显:对 Version 1 的码,因为映射关系退化为仿射变换,效果较差,手机必须和码平面平行才能方便识别。当物料表面不是平面的时候,效果较差。

优化策略:

  • 1)假设采样坐标系到二维码坐标系遵守一个更复杂的映射关系,并且假设物料表面的卷曲较小,通过使用二次函数可以较好的拟合这个映射关系;
  • 2)实际发票上的二维码版本普遍大于等于 7,高版本二维码具有多个辅助定位点,更利于构造二次映射表;
  • 3)基于以上推论,使用新的映射代替旧的透视变换,进行更精准的采样。

用新的策略,发票码这个场景的二维码识别能力提升明显。

▲注意:由于采用了增强算法,请对准二维码稍作等待

样本测试结果:

8.2 容错识别能力提升

商户或者供应商生成二维码后,通常会在二维码的中间部分贴上 Logo,这部分有可能会使二维码 Decode 时出错。

优化策略:

对于采样后拿到的 BitMatrix,对于中间部分一块矩形区域内的点,采用某些策略来改变中间点的值,使它能够通过容错边界的检查。目前采用两种策略,第一种是反转,第二种是每一个点随机取值。目前所取的矩形区域是长、宽的四分之一。

通过此项优化后,扫码的容错能力也得到明显提升。

9、更小的识别耗时

针对识别效率,我们使用了GPU计算二值化,降低识别单帧耗时。

所谓图像二值化就是将图像上的像素点的灰度值设置为 0 或 255,也就是将整个图像呈现出明显的只有黑和白的视觉效果。下图左边为原图,右边是二值化处理过的图。

在扫码算法解码前,有二值化计算,图像的二值化计算能使图像中数据量大为减少,并弱化图像模糊、颜色对比度不强、光线过强/太弱、图像污损等情况下其他信息的干扰,更利于检测识别。

传统算法是在 CPU 上进行二值化运算,非常消耗 CPU 资源,但其实 GPU 更擅长大规模并行计算,所以我们选择使用 GPU 来做二值化计算。在安卓平台上使用 RenderScript,iOS 平台上使用 Metal,都是很底层的框架。

1)iOS优化结果:统一电池、角度、光线等环境变量, 在iPhone6上测试扫码核心5种摄像头二值化算法。

表现如下: 

 

可以看出,在图像二值化方面 Metal 有相当高的优势,相比原来的单纯 CPU 处理快了接近 150%, 同时降低了近50个百分点的CPU资源。

2)Andriod优化结果:由于Android机型众多,我们抽取了线上数据,可以看到GPU 在二值化处理中显著降低了单帧耗时30%以上。 

10、算法分级、场景分类、科学调度

线下物料千奇百怪,扫码算法为了解决一些不理想的场景,如二维码有遮挡、污损、模糊或角度很不好的特殊情况,需要使用一些比较耗时但比较强大的算法,但普通情况不需要这些算法。

所以,我们对识码算法定了优先级,通过时间推移、跳帧触发等方式调度。

优先级:

  • 1)高优先级:每帧执行;
  • 2)中优先级:降帧率执行;
  • 3)低优先级:低帧率执行。

不同优先级的功能执行时机可配置。不同功能属于哪个优先级可配置

特殊场景算法能力:

  • 1)反色码识别能力;
  • 2)容错边界码识别能力;
  • 3)污损桩点识别能力等;
  • 4)条码识别能力。

附录:更多阿里团队分享的文章

社交软件红包技术解密(七):支付宝红包的海量高并发技术实践

阿里钉钉技术分享:企业级IM王者——钉钉在后端架构上的过人之处

来自阿里OpenIM:打造安全可靠即时通讯服务的技术实践分享

淘宝技术分享:手淘亿级移动端接入层网关的技术演进之路

阿里技术分享:深度揭秘阿里数据库技术方案的10年变迁史

阿里技术分享:阿里自研金融级数据库OceanBase的艰辛成长之路

作者谈《阿里巴巴Java开发手册(规约)》背后的故事

淘宝技术分享:手淘亿级移动端接入层网关的技术演进之路

《阿里巴巴Android开发手册(规约)》背后的故事

手机淘宝消息推送系统的架构与实践(音频+PPT) [附件下载]

重磅发布:《阿里巴巴Android开发手册(规约)》[附件下载]

阿里技术结晶:《阿里巴巴Java开发手册-v1.6.0-泰山版》[附件下载]

(本文同步发布于:http://www.52im.net/thread-3150-1-1.html

posted @ 2020-09-25 14:29 Jack Jiang 阅读(303) | 评论 (0)编辑 收藏

本文在编写时参考了博客作者“鹿呦呦”和在线课程“即时消息技术剖析与实战”的相关资料,一并表示感谢。

1、引言

随着移动互联网络的发展,IM技术的应用已经不仅限于聊天应用本身,它早已融入各种应用形态中,比如:直播中的主播互动、联网游戏中的玩家互动、外卖/打车应用中的实时位置共享、在线教育应用中的互动白板等。

在这些风格迥异的应用场景下,IM技术所呈现出来的功能形态虽有不同,但“实时性”这个技术特征并无区别。

那么,对于技术门外汉来说,到底什么是IM的“实时性”?该如何理解它?这就是本文想要讨论的主题。

区别于强大的原生应用,Web端的IM系统,在很长一段时间内想实现真正的“实时性”,是非常困难的,因为无法直接使用UDP、TCP通信协议,在HTML5中的WebSocket出现之前,Web端几乎没有真正意义上的“双向实时通信”这种技术存在。

正因为如此,理解Web端即时通信技术的演进,也就自然而然能循序渐进地体会到IM系统中的“实时性”了。所以本文将围绕Web端即时通讯技术,为你展开IM“实时性”这个话题。

友情提示:本系列文章侧重于理论概念的讲述,篇幅有限,点到即止,如需系统、深入、具体地学习IM技术的方方面面,请从此文入手:《新手入门一篇就够:从零开发移动端IM》(史诗级文章,适合从入门到放弃)。

学习交流:

- 即时通讯/推送技术开发交流5群:215477170 [推荐]

- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM

- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK

(本文同步发布于:http://www.52im.net/thread-3143-1-1.html

2、系列文章目录

IM开发快速入门(一):什么是IM系统?

IM开发快速入门(二):什么是IM系统的实时性?》(* 本文

《IM开发快速入门(三):什么是IM系统的可靠性? (稍后发布)》

《IM开发快速入门(四):什么是IM系统的一致性? (稍后发布)》

《IM开发快速入门(五):什么是IM系统的安全性? (稍后发布)》

《IM开发快速入门(六):什么是IM系统的的心跳机制? (稍后发布)》

《IM开发快速入门(七):如何理解并实现IM系统消息未读数? (稍后发布)》

《IM开发快速入门(八):如何理解并实现IM系统的多端消息漫游? (稍后发布)》

3、短轮询技术

在早期的Web时代,技术的创造者们无法预见如今各种选进的技术应用形式,他们认为数据只是用来“看”的,也数据的获取基本就是“请求 -> 响应”这种一问一答形式。包括我们平时浏览的各种门户网站都是采用的“请求响应”模式。

这种依赖于用户“主动”请求的数据获取模式,如果想实现IM系统,是无法即时获得最新的聊天消息的,因为用户并不知道新消息什么时候到来,而服务端也没有办法主动通知用户。

在这个时期,虽然技术和思路都受当时技术水平的限制,但IM总不能不做吧。

于是,一种被称为“短轮询”的数据获取模式出现了。在“短轮询”模式下,IM客户端定时轮询服务端,以便让用户知道是否有新的聊天消息存在。

这种模式下,服务端收到请求后,即刻查询是否存在新消息,有就返回给客户端,没有则返回空并立即关闭连接。

相较于前面用户需要“手动”去刷新页面的方式,这种模式只是将用户的“手动”变为“自动”而已,技术本质并没有发生任何实质性改变。

短轮询这种模式,就好比旧时代一个等待重要邮件的人,他需要每天自已跑到邮局,主动去问是否有自己的信件,有就拿回家,如果没有,则第二天继续去问。一来一去,非常低效。

技术原理总结如下图所示:

短轮询这种模式有好处,也有坏处。

好处是:

  • 1)技术简单,容易实现;
  • 2)可维护性强,因为它没什么复杂的。

坏处是:

  • 1)因为无法预知数据是否存在,所以多数请求是无用的,浪费计算资源;
  • 2)为了提升实时性,高频率的请求会加大服务端的性能负载。

总结一下就是,短轮询这种模式对于IM技术大拿来说,显的非常low,因为技术实现实在是简单粗暴。

4、长轮询技术

正如你所见,用短轮询技术来保证IM的实时性,确实难说优雅。不过,这难不倒无所不能的程序员,一种被称为“长轮询”的数据获取模式出现了。

从技术上来说,长轮询实现的IM相较于短轮询最大的改进在于:短轮询情况下,服务端不管有没有新消息,请求结束就会立即断开连接。而长轮询时,如果本次请求没有新消息发生,糨不会马上断开连接并返回,而是会将本次连接“挂起”一段时间,如果在这段“挂起”时间内有新的聊天消息出现,就能马上读取并立即返回给客户端,接着结束本次连接。一段时间后又会再次发起请求,如此周而复始。

长轮询这种模式,拿上节等待邮件的这个例子来说,就好比收信的人每天到邮局去问是否有信件,如果没有,他不马上回家,而是在邮局待上一段时间,如果这段时间过去了,还是没有,就先回家,接着第二天再来。

技术原理总结如下图所示: 

长轮询的优点是:

  • 1)相较于短连询,一定程度降低了服务端请求负载;
  • 2)相较于短连询,实时性有提升,因为它是主动“等”消息。

长轮询的缺点是:

  • 1)长论询模式下,连接“挂起”的这段时间内,服务端需要配合开启单独的消息查询线程,仍然存在无用功;
  • 2)相较于短连询模式,在一次长轮询结束、下次轮询发起前的窗口期内,仍然存在“实时性”盲区。

实际上,在Web端即时通讯技术里,长轮询有个专业的术语叫“Comet”,有兴趣可以详细学习《Comet技术详解:基于HTTP长连接的Web端实时通信技术》。

5、轮询无法实现真正的“实时性”

对于Web端即时通讯技术来说,上面提到的无论是短轮询,还是长轮询,它们都存在“实时性”盲区。

我们回到上两节介绍的短轮询和长轮询技术原理图。

先看看短轮询这张图:

很明显,短轮询在每次轮询结束和下次轮询开始的间隔期内,是无法感知到新消息的,这也便形成了“实时性盲区”。换句话说,短轮询技术在“实时性盲区”内,无法做到“实时”。

再来看看长轮询:

跟短轮询道理一样,长轮询在每次轮询结束和下次轮询开始的间隔期,依然会形成“实时性盲区”。

要理解纠结轮询技术的实时性缺陷,就得了解它们背后的技术——HTTP协议了。

HTTP协议设计的目的,就是为了实现“请求--响应”这种模式的数据交互,也就是众所周之的“短连接”设计。而无论是短轮询还是长轮询,都跳不出HTTP的先天技术逻辑(请求--响应--断开)。

所以,归根到底,想要基于HTTP协议来实现IM,要达到真正的“实时性”,是相当勉强的。因为HTTP设计的目的,就是用“短连接”来简化传统TCP长连接通信带来的复杂性,而IM的实时性恰好要用到的又是TCP的长连接特性,所以这就是个悖论。

要真正实现Web端的IM“实时性”,肯定不能强行HTTP上做文章了,我们需要新的技术。

6、WebSocket让Web端IM真正的“实时性”变成可能

好消息是,HTML5中带来了WebSocket技术。WebSocket是真正的全双式双向通信技术(详见:《WebSocket从入门到精通,半小时就够!》)。

下图上旧式轮询技术跟WebSocket的对比图:

从上图可以看出:

  • 1)轮询技术一问一答,在下一个请求发起之前,存在“实时性”盲区;
  • 2)WebSocket一旦建立连接后,数据可以随时双向通信(即客户端可以随时向服务端发消息,服务端也可以随时通知客户端有新消息)。

举个例子就是:轮询技术相当于传统的邮件传递方法(你得自已去邮局问有没有新邮件),而WebSocket相当于现代的电话系统,只要你拨通后,随时可以实时收听到对方的声音,对方也能随时收听到你的声音。完美!

总结一下WebSocket 的优点是:

  • 1)真正的实时性:支持客户端与服务端真正的双向实时通信;
  • 2)大幅降低负载:少了轮询技术中高频率无用的请求,可大大降低服务端QPS压力;
  • 3)网络开销降低:一次连接,随时使用,再也不用轮询技术中每次发起HTTP请求(随之而来的是每次HTTP的大量冗余协议头信息等)。

7、本文小结

本文以Web端即时通讯技术的演进为例,从短轮询到长轮询,再到WebSocket,理论联系实际地讲解了Web端IM“实时性”的技术变迁,从而帮助读者理解IM中“实时性”这个最为关键的技术特征。

附录:更多Web端即时通讯资料

新手入门贴:史上最全Web端即时通讯技术原理详解

Web端即时通讯技术盘点:短轮询、Comet、Websocket、SSE

SSE技术详解:一种全新的HTML5服务器推送事件技术

Comet技术详解:基于HTTP长连接的Web端实时通信技术

新手快速入门:WebSocket简明教程

WebSocket详解(一):初步认识WebSocket技术

WebSocket详解(二):技术原理、代码演示和应用案例

WebSocket详解(三):深入WebSocket通信协议细节

WebSocket详解(四):刨根问底HTTP与WebSocket的关系(上篇)

WebSocket详解(五):刨根问底HTTP与WebSocket的关系(下篇)

WebSocket详解(六):刨根问底WebSocket与Socket的关系

socket.io实现消息推送的一点实践及思路

LinkedIn的Web端即时通讯实践:实现单机几十万条长连接

Web端即时通讯技术的发展与WebSocket、Socket.io的技术实践

Web端即时通讯安全:跨站点WebSocket劫持漏洞详解(含示例代码)

开源框架Pomelo实践:搭建Web端高性能分布式IM聊天服务器

使用WebSocket和SSE技术实现Web端消息推送

详解Web端通信方式的演进:从Ajax、JSONP 到 SSE、Websocket

MobileIMSDK-Web的网络层框架为何使用的是Socket.io而不是Netty?

理论联系实际:从零理解WebSocket的通信原理、协议格式、安全性

微信小程序中如何使用WebSocket实现长连接(含完整源码)

八问WebSocket协议:为你快速解答WebSocket热门疑问

快速了解Electron:新一代基于Web的跨平台桌面技术

一文读懂前端技术演进:盘点Web前端20年的技术变迁史

Web端即时通讯基础知识补课:一文搞懂跨域的所有问题!

Web端即时通讯实践干货:如何让你的WebSocket断网重连更快速?

WebSocket从入门到精通,半小时就够!

>> 更多同类文章 ……

本文已同步发布于“即时通讯技术圈”公众号:

▲ 本文在公众号上的链接是:点此进入,原文链接是:http://www.52im.net/thread-3143-1-1.html

posted @ 2020-09-18 13:56 Jack Jiang 阅读(209) | 评论 (0)编辑 收藏

1、引言

在中大型IM系统中,聊天消息的唯一ID生成策略是个很重要的技术点。不夸张的说,聊天消息ID贯穿了整个聊天生命周期的几乎每一个算法、逻辑和过程,ID生成策略的好坏有可能直接决定系统在某些技术点上的设计难易度。

有中小型IM场景下,消息ID可以简单处理,反正只要唯一就行,而中大型场景下,因为要考虑到分布式的性能、一致性等,所以要考虑的问题点又是另一回事。

总之就是,IM的消息ID生成这件事,可深可浅,看似简单但实际可探索的边界可以很大,这也是为什么即时通讯网为此专门整理了《IM消息ID技术专题》系列文章的原因。做技术所谓厚积薄发,了解的越多,你的技术可操作空间也就越大,希望随着这个系列文章的阅读,可以为你在ID生成这一块的技术选型带来更多有益的启发。

另外,因为即时通讯网主要关注的是即时通讯方面的系统开发,但并不意味着这个系统文章只适用于IM或消息推送等实时通信系统,它同样适用于其它需要唯一ID的应用中。

本文将要分享的是滴滴开源的分布式ID生成器Tinyid的技术原理、使用方法等等,希望能进一步为你打开这方面的技术视野。

学习交流:

- 即时通讯/推送技术开发交流5群:215477170[推荐]

- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM

- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK

(本文同步发布于:http://www.52im.net/thread-3129-1-1.html

2、专题目录

本文是“IM消息ID技术专题”系列文章的第6篇,专题总目录如下:

IM消息ID技术专题(一):微信的海量IM聊天消息序列号生成实践(算法原理篇)

IM消息ID技术专题(二):微信的海量IM聊天消息序列号生成实践(容灾方案篇)

IM消息ID技术专题(三):解密融云IM产品的聊天消息ID生成策略

IM消息ID技术专题(四):深度解密美团的分布式ID生成算法

IM消息ID技术专题(五):开源分布式ID生成器UidGenerator的技术实现

IM消息ID技术专题(六):深度解密滴滴的高性能ID生成器(Tinyid)》(* 本文

3、什么是Tinyid?

Tinyid是滴滴用Java开发的一款分布式id生成系统,基于数据库号段算法实现。

Tinyid是在美团的ID生成算法Leaf的基础上扩展而来,支持数据库多主节点模式,它提供了REST API和Java客户端两种获取方式,相对来说使用更方便。不过,和美团的Leaf算法不同的是,Tinyid只支持号段一种模式(并不支持Snowflake模式)。(有关美团的Leaf算法,可以详读《IM消息ID技术专题(四):深度解密美团的分布式ID生成算法

Tinyid目前在滴滴客服部门使用,且通过tinyid-client方式接入,每天生成的是亿级别的id。性能上,据称单实例能达到1千万QPS。

它的开源地址是:

PS:滴滴在Tinyid工程页面写了一句话,“tinyid,并不是滴滴官方产品,只是滴滴拥有的代码”,我语文不好,这句该怎么理解呢?

4、Tinyid的主要技术特性

主要特性总结一下就是:

  • 1)全局唯一的long型ID:即id极限数量是2的64次方;
  • 2)趋势递增的id:趋势递增的意思是,id是递增但不一定是连续的(这跟微信的ID生成策略类似);
  • 3)提供 http 和 java-client 方式接入;
  • 4)支持批量获取ID;
  • 5)支持生成1,3,5,7,9…序列的ID;
  • 6)支持多个db的配置。

适用的场景:只关心ID是数字,趋势递增的系统,可以容忍ID不连续,可以容忍ID的浪费。

不适用场景:像类似于订单ID的业务,因生成的ID大部分是连续的,容易被扫库、或者推算出订单量等信息。

另外:微信的聊天消息ID生成算法也是基于号段、趋势递增这种逻辑,如果有兴趣,可以详见:《IM消息ID技术专题(一):微信的海量IM聊天消息序列号生成实践(算法原理篇)》。

5、Tinyid的技术优势

性能方面:

  • 1)http方式:访问性能取决于http server的能力,网络传输速度;
  • 2)java-client方式:id为本地生成,号段长度(step)越长,qps越大,如果将号段设置足够大,则qps可达1000w+。

可用性方面:

  • 1)当db不可用时,因为server有缓存,所以还可以使用一段时间;
  • 2)如果配置了多个db,则只要有1个db存活,则服务可用;
  • 3)使用tiny-client时,只要server有一台存活,则理论上server全挂,因为client有缓存,也可以继续使用一段时间。

6、Tinyid的技术原理详解

6.1 ID生成系统的技术要点

在简单系统中,我们常常使用db的id自增方式来标识和保存数据,随着系统的复杂,数据的增多,分库分表成为了常见的方案,db自增已无法满足要求。

这时候全局唯一的id生成系统就派上了用场,当然这只是id生成其中的一种应用场景。

那么,一个成熟的id生成系统应该具备哪些能力呢?

  • 1)唯一性:无论怎样都不能重复,id全局唯一是最基本的要求;
  • 2)高性能:基础服务尽可能耗时少,如果能够本地生成最好;
  • 3)高可用:虽说很难实现100%的可用性,但是也要无限接近于100%的可用性;
  • 4)易用性:能够拿来即用,接入方便,同时在系统设计和实现上要尽可能的简单。

6.2 Tinyid的实现原理

我们先来看一下最常见的id生成方式,db的auto_increment,相信大家都非常熟悉。

我也见过一些同学在实战中使用这种方案来获取一个id,这个方案的优点是简单,缺点是每次只能向db获取一个id,性能比较差,对db访问比较频繁,db的压力会比较大。

那么,是不是可以对这种方案优化一下呢?可否一次向db获取一批id呢?答案当然是可以的。

一批id,我们可以看成是一个id范围,例如(1000,2000],这个1000到2000也可以称为一个“号段”,我们一次向db申请一个号段,加载到内存中,然后采用自增的方式来生成id,这个号段用完后,再次向db申请一个新的号段,这样对db的压力就减轻了很多,同时内存中直接生成id,性能则提高了很多。

PS:简单解释一下什么是号段模式:

号段模式就是从数据库批量的获取自增ID,每次从数据库取出一个号段范围,例如 (1,1000] 代表1000个ID,业务服务将号段在本地生成1~1000的自增ID并加载到内存。

那么保存db号段的表该怎设计呢?我们继续往下看。

6.3 DB号段算法描述

如上表,我们很容易想到的是db直接存储一个范围(start_id,end_id],当这批id使用完毕后,我们做一次update操作,update start_id=2000(end_id), end_id=3000(end_id+1000),update成功了,则说明获取到了下一个id范围。仔细想想,实际上start_id并没有起什么作用,新的号段总是(end_id,end_id+1000]。

所以这里我们更改一下,db设计应该是这样的:

如上表所示:

  • 1)我们增加了biz_type,这个代表业务类型,不同的业务的id隔离;
  • 2)max_id则是上面的end_id了,代表当前最大的可用id;
  • 3)step代表号段的长度,可以根据每个业务的qps来设置一个合理的长度;
  • 4)version是一个乐观锁,每次更新都加上version,能够保证并发更新的正确性 。

那么我们可以通过如下几个步骤来获取一个可用的号段:

A、查询当前的max_id信息:select id, biz_type, max_id, step, version from tiny_id_info where biz_type='test';

B、计算新的max_id: new_max_id = max_id + step;

C、更新DB中的max_id:update tiny_id_info set max_id=#{new_max_id} , verison=version+1 where id=#{id} and max_id=#{max_id} and version=#{version};

D、如果更新成功,则可用号段获取成功,新的可用号段为(max_id, new_max_id];

E、如果更新失败,则号段可能被其他线程获取,回到步骤A,进行重试。

6.4 号段生成方案的简单架构

如上述内容,我们已经完成了号段生成逻辑。

那么我们的id生成服务架构可能是这样的:

如上图,id生成系统向外提供http服务,请求经过我们的负载均衡router,到达其中一台tinyid-server,从事先加载好的号段中获取一个id。

如果号段还没有加载,或者已经用完,则向db再申请一个新的可用号段,多台server之间因为号段生成算法的原子性,而保证每台server上的可用号段不重,从而使id生成不重。

可以看到:

  • 1)如果tinyid-server如果重启了,那么号段就作废了,会浪费一部分id;
  • 2)同时id也不会连续;
  • 3)每次请求可能会打到不同的机器上,id也不是单调递增的,而是趋势递增的(不过这对于大部分业务都是可接受的)。

6.5 简单架构的问题

到此一个简单的id生成系统就完成了,那么是否还存在问题呢?

回想一下我们最开始的id生成系统要求:高性能、高可用、简单易用。

在上面这套架构里,至少还存在以下问题:

  • 1)当id用完时需要访问db加载新的号段,db更新也可能存在version冲突,此时id生成耗时明显增加;
  • 2)db是一个单点,虽然db可以建设主从等高可用架构,但始终是一个单点;
  • 3)使用http方式获取一个id,存在网络开销,性能和可用性都不太好。

6.6 优化办法及最终架构

1)双号段缓存:

对于号段用完需要访问db,我们很容易想到在号段用到一定程度的时候,就去异步加载下一个号段,保证内存中始终有可用号段,则可避免性能波动。

2)增加多db支持:

db只有一个master时,如果db不可用(down掉或者主从延迟比较大),则获取号段不可用。实际上我们可以支持多个db,比如2个db,A和B,我们获取号段可以随机从其中一台上获取。那么如果A,B都获取到了同一号段,我们怎么保证生成的id不重呢?tinyid是这么做的,让A只生成偶数id,B只生产奇数id,对应的db设计增加了两个字段,如下所示

delta代表id每次的增量,remainder代表余数,例如可以将A,B都delta都设置2,remainder分别设置为0,1则,A的号段只生成偶数号段,B是奇数号段。通过delta和remainder两个字段我们可以根据使用方的需求灵活设计db个数,同时也可以为使用方提供只生产类似奇数的id序列。

3)增加tinyid-client:

使用http获取一个id,存在网络开销,是否可以本地生成id?

为此我们提供了tinyid-client,我们可以向tinyid-server发送请求来获取可用号段,之后在本地构建双号段、id生成,如此id生成则变成纯本地操作,性能大大提升,因为本地有双号段缓存,则可以容忍tinyid-server一段时间的down掉,可用性也有了比较大的提升。

4)tinyid最终架构:

最终我们的架构可能是这样的:

下面是更具体的代码调用逻辑:

如上图所示,下面是关于这个代码调用逻辑图的说明:

  • 1)nextId和getNextSegmentId是tinyid-server对外提供的两个http接口;
  • 2)nextId是获取下一个id,当调用nextId时,会传入bizType,每个bizType的id数据是隔离的,生成id会使用该bizType类型生成的IdGenerator;
  • 3)getNextSegmentId是获取下一个可用号段,tinyid-client会通过此接口来获取可用号段;
  • 4)IdGenerator是id生成的接口;
  • 5)IdGeneratorFactory是生产具体IdGenerator的工厂,每个biz_type生成一个IdGenerator实例。通过工厂,我们可以随时在db中新增biz_type,而不用重启服务;
  • 6)IdGeneratorFactory实际上有两个子类IdGeneratorFactoryServer和IdGeneratorFactoryClient,区别在于,getNextSegmentId的不同,一个是DbGet,一个是HttpGet;
  • 7)CachedIdGenerator则是具体的id生成器对象,持有currentSegmentId和nextSegmentId对象,负责nextId的核心流程。nextId最终通过AtomicLong.andAndGet(delta)方法产生。

具体的代码实现,有兴趣可以直接阅读源码:

7、Tinyid的最佳实践

1)tinyid-server推荐部署到多个机房的多台机器:

多机房部署可用性更高,http方式访问需使用方考虑延迟问题。

2)推荐使用tinyid-client来获取id,好处如下:

a、id为本地生成(调用AtomicLong.addAndGet方法),性能大大增加;

b、client对server访问变的低频,减轻了server的压力;

c、因为低频,即便client使用方和server不在一个机房,也无须担心延迟;

d、即便所有server挂掉,因为client预加载了号段,依然可以继续使用一段时间

注:使用tinyid-client方式,如果client机器较多频繁重启,可能会浪费较多的id,这时可以考虑使用http方式。

3)推荐db配置两个或更多:

db配置多个时,只要有1个db存活,则服务可用 多db配置,如配置了两个db,则每次新增业务需在两个db中都写入相关数据。

8、Tinyid该怎么调用?

关于怎么调用。鉴于篇幅原因,就不再具体去写了,有兴趣的话,可以读一下这篇《Tinyid:滴滴开源千万级并发的分布式ID生成器》。

9、参考资料

[1] 面试总被问分布式ID怎么办? 滴滴(Tinyid)甩给他

[2] Tinyid:滴滴开源千万级并发的分布式ID生成器

[3] tinyid工程中文readme

[4] 滴滴开源的Tinyid如何每天生成亿级别的ID?

附录:更多IM开发热门技术文章

新手入门一篇就够:从零开发移动端IM

移动端IM开发者必读(一):通俗易懂,理解移动网络的“弱”和“慢”

移动端IM开发者必读(二):史上最全移动弱网络优化方法总结

从客户端的角度来谈谈移动端IM的消息可靠性和送达机制

现代移动端网络短连接的优化手段总结:请求速度、弱网适应、安全保障

移动端IM中大规模群消息的推送如何保证效率、实时性?

移动端IM开发需要面对的技术问题

开发IM是自己设计协议用字节流好还是字符流好?

请问有人知道语音留言聊天的主流实现方式吗?

IM消息送达保证机制实现(一):保证在线实时消息的可靠投递

IM消息送达保证机制实现(二):保证离线消息的可靠投递

如何保证IM实时消息的“时序性”与“一致性”?

一个低成本确保IM消息时序的方法探讨

IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?

IM群聊消息如此复杂,如何保证不丢不重?

谈谈移动端 IM 开发中登录请求的优化

移动端IM登录时拉取数据如何作到省流量?

浅谈移动端IM的多点登录和消息漫游原理

完全自已开发的IM该如何设计“失败重试”机制?

自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)

IM开发基础知识补课(六):数据库用NoSQL还是SQL?读这篇就够了!

适合新手:从零开发一个IM服务端(基于Netty,有完整源码)

拿起键盘就是干:跟我一起徒手开发一套分布式IM系统

适合新手:手把手教你用Go快速搭建高性能、可扩展的IM系统(有源码)

IM里“附近的人”功能实现原理是什么?如何高效率地实现它?

IM开发基础知识补课(七):主流移动端账号登录方式的原理及设计思路

IM要做手机扫码登录?先看看微信的扫码登录功能技术原理

IM开发宝典:史上最全,微信各种功能参数和逻辑规则资料汇总

IM开发干货分享:我是如何解决大量离线消息导致客户端卡顿的

IM开发干货分享:如何优雅的实现大量离线消息的可靠投递

IM开发干货分享:有赞移动端IM的组件化SDK架构设计实践

>> 更多同类文章 ……

本文已同步发布于“即时通讯技术圈”公众号,欢迎关注:

▲ 本文在公众号上的链接是:点此进入,原文链接是:http://www.52im.net/thread-3129-1-1.html

posted @ 2020-09-08 22:31 Jack Jiang 阅读(367) | 评论 (0)编辑 收藏

     摘要: 本文由小米信息技术团队研发工程师陈刚原创,原题“当我们在谈论高并发的时候究竟在谈什么?”,为了更好的内容呈现,即时通讯网收录时有修订和改动。1、引言在即时通讯网社区里,多是做IM、消息推送、客服系统、音视频聊天这类实时通信方面的开发者,在涉及到即时通讯技术时聊的最多的话题就是高并发、高吞吐、海量用户。代码还没开始写,就考虑万一哪天这IM用户量破百万、千万该怎么办的问题,是多...  阅读全文

posted @ 2020-09-03 23:08 Jack Jiang 阅读(303) | 评论 (0)编辑 收藏

本文内容编写时,参考了网上的资料,详见“参考资料”部分,感谢分享者。

1、引言

这个系列文章已经整理了10篇,但都没有涉及到具体的红包算法实现,主要有以下两方面原因。

一方面是各社交/IM产品中的红包功能同质化严重,红包算法的“可玩性”便是“核心竞争力所在”,这是同质化功能的差异化竞争思路,不会随便公开。

另一方面,市场上还存在各种抢红包插件这类灰产存在,一旦公开这些算法,很可能又被这帮插件开发者们搞出什么幺蛾子。

所以,这样的情况下,如果要做社交/IM产品中的红包功能,红包随便算法该怎么实现,基本上只能自已琢磨,很难找到大厂算法直接套用。

本着即时通讯网一贯的im知识传播精神,我收集整理并参考了大量的网上资料,综合了比较靠谱的信息来源,便有了本文。本文根据有限的资料,分享了微信红包随机算法实现中的一些技术要点,并整理了两种比较靠谱的红包算法实现思路(含可运行的实现代码),希望能给你的红包算法开发带来启发。

申明:本文资料整理自网络,仅供学习研究之用,如有不妥,请通知作者。

学习交流:

- 即时通讯开发交流5群:215477170 [推荐]

- 移动端IM开发入门文章:新手入门一篇就够:从零开发移动端IM

- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK  [推荐]

本文已同步发布于“即时通讯技术圈”公众号,欢迎关注:

▲ 本文在公众号上的链接是:点此进入,原文链接是:http://www.52im.net/thread-3125-1-1.html

2、系列文章

3、微信红包算法要点汇总

这是目前能找到的仅有的一份,有微信团队人员参与的微信红包算法技术要点的讨论资料。分享于2015年,差不多是微信红包刚火没多久,大概是微信技术团队的人当时没有现在这些技术之外的顾虑,所以作了有限的分享,资料难得,本次重新整理了一下,可以作为参考资料使用。以下是资料正文。

资料来源:来自InfoQ的某架构群的技术讨论,由朱玉华整理(个人博客是:zhuyuhua.com(目前已无法访问))。

资料背景:起因是有朋友在朋友圈咨询微信红包的架构,于是在微信团队成员参与讨论的情况下,我(指“朱玉华”)整理了这次讨论的技术要点,也就是下面的内容(内容为问答形式)。

3.1、算法实现的技术要点

问:微信的金额什么时候算?

答:微信金额是拆的时候实时算出来,不是预先分配的,采用的是纯内存计算,不需要预算空间存储。

为什么采取实时计算金额?原因是:实时效率更高,预算才效率低下。预算还要占额外存储。因为红包只占一条记录而且有效期就几天,所以不需要多大空间。就算压力大时,水平扩展机器是。

问:关于实时实时性,为什么明明抢到红包,点开后发现没有?

答:2014年的红包一点开就知道金额,分两次操作,先抢到金额,然后再转账。

2015年的红包的拆和抢是分离的,需要点两次,因此会出现抢到红包了,但点开后告知红包已经被领完的状况。进入到第一个页面不代表抢到,只表示当时红包还有。

问:关于分配算法,红包里的金额怎么算?为什么出现各个红包金额相差很大?

答:随机,额度在 0.01 和剩余平均值 2 之间。 例如:发 100 块钱,总共 10 个红包,那么平均值是 10 块钱一个,那么发出来的红包的额度在 0.01元~20元之间波动。

当前面 3 个红包总共被领了 40 块钱时,剩下 60 块钱,总共 7 个红包,那么这 7 个红包的额度在:0.01~(60/7 * 2)=17.14之间。

注意:这里的算法是每被抢一个后,剩下的会再次执行上面的这样的算法(Tim老师也觉得上述算法太复杂,不知基于什么样的考虑)。

这样算下去,会超过最开始的全部金额,因此到了最后面如果不够这么算,那么会采取如下算法:保证剩余用户能拿到最低1分钱即可。

如果前面的人手气不好,那么后面的余额越多,红包额度也就越多,因此实际概率一样的。

问:红包的设计

答:微信从财付通拉取金额数据过来,生成个数/红包类型/金额放到redis集群里,app端将红包ID的请求放入请求队列中,如果发现超过红包的个数,直接返回。根据红包的逻辑处理成功得到令牌请求,则由财付通进行一致性调用,通过像比特币一样,两边保存交易记录,交易后交给第三方服务审计,如果交易过程中出现不一致就强制回归。

问:并发性处理:红包如何计算被抢完?

答:cache会抵抗无效请求,将无效的请求过滤掉,实际进入到后台的量不大。cache记录红包个数,原子操作进行个数递减,到 0 表示被抢光。财付通按照 20万笔每秒入账准备,但实际还不到 8万每秒

问:通如何保持8w每秒的写入?

答:多主sharding,水平扩展机器。

问:数据容量多少?

答:一个红包只占一条记录,有效期只有几天,因此不需要太多空间。

问:查询红包分配,压力大不?

答:抢到红包的人数和红包都在一条cache记录上,没有太大的查询压力。

问:一个红包一个队列?

答:没有队列,一个红包一条数据,数据上有一个计数器字段。

问:有没有从数据上证明每个红包的概率是不是均等?

答:不是绝对均等,就是一个简单的拍脑袋算法。

问:拍脑袋算法,会不会出现两个最佳?

答:会出现金额一样的,但是手气最佳只有一个,先抢到的那个最佳。

问:每领一个红包就更新数据么?

答:每抢到一个红包,就cas更新剩余金额和红包个数。

问:红包如何入库入账?

答:数据库会累加已经领取的个数与金额,插入一条领取记录。入账则是后台异步操作。

问:入帐出错怎么办?比如红包个数没了,但余额还有?

答:最后会有一个take all操作。另外还有一个对账来保障。

问:既然在抢的时候有原子减了就不应该出现抢到了拆开没有的情况?

答:这里的原子减并不是真正意义上的原子操作,是Cache层提供的CAS,通过比较版本号不断尝试。

问:cache和db挂了怎么办?

答:主备 +对账。

问:为什么要分离抢和拆?

答:总思路是设置多层过滤网,层层筛选,层层减少流量和压力。

这个设计最初是因为抢操作是业务层,拆是入账操作,一个操作太重了,而且中断率高。 从接口层面看,第一个接口纯缓存操作,搞压能力强,一个简单查询Cache挡住了绝大部分用户,做了第一道筛选,所以大部分人会看到已经抢完了的提示。

问:抢到红包后再发红包或者提现,这里有什么策略吗?

答:大额优先入账策略。

针对上面的技术要点,有人还画了张原理图(这是网上能找到的相对清晰的版本): 

3.2、微信抢红包的过程模拟

针对上节中整理的资料,当有人在微信群里发了一个 N 人的红包、总金额 M 元,后台大概的技术逻辑如下。

3.2.1)发红包后台操作:

  • 1)在数据库中增加一条红包记录,存储到CKV,设置过期时间;
  • 2)在Cache(可能是腾讯内部kv数据库,基于内存,有落地,有内核态网络处理模块,以内核模块形式提供服务))中增加一条记录,存储抢红包的人数N。

3.2.2)抢红包后台操作:

  • 1)抢红包分为抢和拆:抢操作在Cache层完成,通过原子减操作进行红包数递减,到0就说明抢光了,最终实际进入后台拆操作的量不大,通过操作的分离将无效请求直接挡在Cache层外面。
  • 这里的原子减操作并不是真正意义上的原子减操作,是其Cache层提供的CAS,通过比较版本号不断尝试,存在一定程度上的冲突,冲突的用户会放行,让其进入下一步拆的操作,这也解释了为啥有用户抢到了拆开发现领完了的情况。
  • 2)拆红包在数据库完成:通过数据库的事务操作累加已经领取的个数和金额,插入一条领取流水,入账为异步操作,这也解释了为啥在春节期间红包领取后在余额中看不到。
  • 拆的时候会实时计算金额,其金额为1分到剩余平均值2倍之间随机数,一个总金额为M元的红包,最大的红包为 M * 2 /N(且不会超过M),当拆了红包后会更新剩余金额和个数。财付通按20万笔每秒入账准备,实际只到8万每秒。

4、微信红包算法模拟实现1(含代码)

根据上一节的微信红包随机算法技术要点资料,实现了一个算法,以下供参考。(注:本节内容引用自《微信红包随机算法初探》一文)

4.1、算法约定

算法很简单,跟微信的算法一样,不是提前算好,而是抢红包时计算。

即:金额随机,额度在0.01和剩余平均值*2之间。(参见上一节的 “关于分配算法,红包里的金额怎么算?为什么出现各个红包金额相差很大?” 内容)

4.2、代码实现

算法的逻辑主要是:

public static double getRandomMoney(RedPackage _redPackage) {

    // remainSize 剩余的红包数量

    // remainMoney 剩余的钱

    if(_redPackage.remainSize == 1) {

        _redPackage.remainSize--;

        return (double) Math.round(_redPackage.remainMoney * 100) / 100;

    }

    Random r     = newRandom();

    double min   = 0.01; //

    double max   = _redPackage.remainMoney / _redPackage.remainSize * 2;

    double money = r.nextDouble() * max;

    money = money <= min ? 0.01: money;

    money = Math.floor(money * 100) / 100;

    _redPackage.remainSize--;

    _redPackage.remainMoney -= money;

    return money;

}

LeftMoneyPackage数据结构如下:

class RedPackage {

    int remainSize;

    double remainMoney;

}

测试时初始化相关数据是:

static void init() {

    redPackage.remainSize  = 30;

    redPackage.remainMoney = 500;

}

附件是可以运行的完整Java代码文件:

(无法上传附件,如有需要请从此链接处下载:http://www.52im.net/thread-3125-1-1.html

4.3、测试结果

4.3.1 单次测试

按上述代码中的初始化数据(30人抢500块),执行了两次,结果如下:

//第一次

15.69   21.18   24.11   30.85   0.74    20.85   2.96    13.43   11.12   24.87   1.86    19.62   5.97    29.33   3.05    26.94   18.69   34.47   9.4 29.83   5.17    24.67   17.09   29.96   6.77    5.79    0.34    23.89   40.44   0.92

//第二次

10.44   18.01   17.01   21.07   11.87   4.78    30.14   32.05   16.68   20.34   12.94   27.98   9.31    17.97   12.93   28.75   12.1    12.77   7.54    10.87   4.16    25.36   26.89   5.73    11.59   23.91   17.77   15.85   23.42   9.77

第一次随机红包数据图表如下: 

▲ x轴为抢的顺序,y轴为抢到的金额

第二次随机红包数据图表如下:

▲ x轴为抢的顺序,y轴为抢到的金额

4.3.2 多次均值

重复执行200次的均值:

▲ x轴为抢的顺序,y轴为该次抢到金额的概率均值

重复执行2000次的均值: 

▲ x轴为抢的顺序,y轴为该次抢到金额的概率均值

从以上两张图的均值结果可以看出,这个算法中每一次能抢到的金额几率几乎是均等的,从随机性来说比较合理。

5、微信红包算法模拟实现2(含代码)

我对随机算法很感兴趣,正巧最近研究的方向有点偏随机数这块,所以也自己实现了一下微信的红包分发算法(算法要点参考的是本文第三节内容)。(注:本节内容引用自《微信红包算法的分析》一文)

5.1、代码实现

从第三节中可以了解到,微信并不是一开始就预分配所有的红包金额,而是在拆时进行计算的。这样做的好处是效率高,实时性。本次的代码中,红包具体是怎么计算的呢?请参见第4节中的“关于分配算法,红包里的金额怎么算?为什么出现各个红包金额相差很大?”。

那基于这个思想,可以写出一个红包分配算法:

/**

 * 并不完美的红包算法

 */

public static double rand(double money, int people, List<Double> l) {

    if(people == 1) {

        double red = Math.round(money * 100) / 100.0;

        l.add(red);

        return0;

    }

    Random random = newRandom();

    double min = 0.01;

    double max = money / people * 2.0;

    double red = random.nextDouble() * max;

    red = red <= min ? min : red;

    red = Math.floor(red * 100) / 100.0;

    l.add(red);

    double remain = Math.round((money - red) * 100) / 100.0;

    return remain;

}

算法整体思路很简单,就在在最后一个人的时候要注意,此时不进行随机数计算,而是直接将剩余金额作为红包。

5.2、第一次分析

采用上述算法,可以对用户的抢红包行为做分析。这里的模仿行为是:30 元的红包,10 人抢。操作 100 次。

可以得出如下结果: 

▲ x轴为抢的顺序,y轴为该次抢到金额

从上图中可以很轻易的看出来,越后抢的人,风险越大,同时收益也越大,有较大几率获得“手气最佳”。

那红包面值的分布性如何呢?

▲ x轴为抢的顺序,y轴为该次抢到金额重复 100 次后的平均值

从上图可以看出,都是比较接近平均值(3 元)的。

那重复 1000 次呢?

▲ x轴为抢的顺序,y轴为该次抢到金额重复 1000 次后的平均值

更接近了。。。

可以看出,这个算法可以让大家抢到红包面额在概率上是大致均等的。

5.3、不足之处

有人提出了这个问题: 

他接下来放了好几张他试验的截图。我这里取了一张,如果有兴趣,可以去知乎的问题里查看更多图片。

而此时,我哥们在和我的在讨论中,也告诉我,确实存在某个规律,可能让最后一个抢的人占有某些微小的优势,比如,多 0.01 的之类。

例如发 6 个,总额 0.09 的包,最后一个抢的有极大概率是 0.03。

然而我之前的代码却没办法体现出这一点。

比如 10 人拆 0.11 元的包,我的结果是:

可见以上代码还存在不足之处。

于是我就有一个猜测:

微信可能不是对全金额进行随机的,可能在派发红包之前,已经对金额做了处理,比如,事先减去(红包个数*0.01),之后在每个红包的随机值基础上加 0.01,以此来保证每个红包最小值都是 0.01。

这个猜测或许可以解开那位知友和我哥们这边的疑惑。

5.4、完善算法

在原先的基础上对代码进行简单的修正:

public static double rand(double money, int people, List<Double> l) {

    if(people == 1) {

        double red = Math.round(money * 100) / 100.0;

        l.add(red+0.01);

        return 0;

    }

    Random random = newRandom();

    double min = 0;

    double max = money / people * 2.0;

    double red = random.nextDouble() * max;

    red = red <= min ? min : red;

    red = Math.floor(red * 100) / 100.0;

    l.add(red+0.01);

    double remain = Math.round((money - red) * 100) / 100.0;

    return remain;

}

这个算法,在第一次调用时传入 money 的值是总金额减去红包数*0.01,大概像这样:

_money = _money - people * 0.01;

5.5、第二次分析

5.5.1 验证上次的不足之处

1)10 人抢 0.11 元的包:

2)2 人抢 0.03 元的包: 

3)6 人抢 0.09 的包:

5.5.2 修改后的代码会不会对已知结论造成影响?

30 元的红包,10 人抢,操作 100 次。

▲ x轴为抢的顺序,y轴为该次抢到金额 

▲ x轴为抢的顺序,y轴为该次抢到金额重复 100 次后的平均值

由上面两图可见,结论基本上没有改变。

5.6、结论

经过上述代码实践可知:

1)先抢后抢,金额期望都是相同的;

2)微信的红包算法很可能是预先分配给每人 0.01 的“底额”;

3)后抢者风险高,收益大。

5.7、补充

上几张后面测试的图,补充一下之前的观点,发 n 个红包,总金额是(n+1)*0.01,最后一个领的一定是手气最佳。

 

大家也可以试试。

以上,大概可以证明,微信红包是在分配前先给每个人 0.01 的最低金额的!

6、参考资料

[1] 微信红包随机算法初探

[2] 微信红包算法的分析

[3] 微信红包的架构设计简介

[4] 微信红包的随机算法是怎样实现的?

另外,知乎上对于微信红包算法的讨论问题很多人参与,有兴趣可以上去看看,或许会有更多启发:《微信红包的随机算法是怎样实现的?》。

附录:更多微信相关资源

IM开发宝典:史上最全,微信各种功能参数和逻辑规则资料汇总

微信本地数据库破解版(含iOS、Android),仅供学习研究 [附件下载]

(本文同步发布于:http://www.52im.net/thread-3125-1-1.html

posted @ 2020-08-26 14:32 Jack Jiang 阅读(295) | 评论 (0)编辑 收藏

仅列出标题
共51页: First 上一页 27 28 29 30 31 32 33 34 35 下一页 Last 
Jack Jiang的 Mail: jb2011@163.com, 联系QQ: 413980957, 微信: hellojackjiang