一直在纠结于先完成同步异步模块,还是消息队列模块,具体原因也不清楚。
也许是因为刚接触到消息队列的时候,被它的魅力所震撼,等不及开始进行整理。
但又觉得,如果将同步异步的整理地更加清楚的话,也许消息队列可能会完成地更加完整和全面。
也因为如此,以至于迟迟没开始整理。但总是需要开始的,所以选择了后者。
正如前面所说,刚接触消息队列的,被它的魅力所震撼。以往,总是觉得这个概念是遥不可及的,且可有可无的,但是当真正需要考虑的时候,才发现,原来它一直在那里,等着我们的学习与使用。
另外,其实并不擅长描述文字性的内容,更喜欢使用代码实现,但是消息队列有多种,例如RabbitMQ,RocketMQ等等,每一种都有各自的特点,所以这篇更关注于内容的理解,而不是实现。
同步异步(wx.request+ajax)
消息队列的学习与理解
消息队列之RabbitMQ的学习
RabbitMQ的应用之spring-boot-starter-amqp
牛逼哄哄的RabbitMQ到底有啥用
那些年搞不懂的高深术语——依赖倒置•控制反转•依赖注入•面向接口编程
如何保证消息不丢失
阿里RocketMQ如何解决消息的顺序和重复两大硬伤?
消息队列面试连环问
消息中间件面试题系列
消息队列,即Message Queue 很容易联想到MQ,再想到流行的kafka、RabbitMQ、RocketMQ
由于开发经历特别少,所以没什么使用经验。
所以看到一篇写得非常有参考价值楚,已在 参考链接 处标明,虽然是说RabbitMQ,但对于消息队列也是同样的概念。
耦合天生就与自由为敌
关于 解耦 的概念一直有那么个意思,但就是表达不出来。所以查了许多资料,发现这篇不错,
只是生产者和消费者的位置好像弄反了。
什么是解耦:https://www.jianshu.com/p/5543b2eee223
假设生产者和消费者分别是两个类。如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合)。将来如果消费者的代码发生变化,可能会影响到生产者。而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了。*
生产者直接调用消费者的某个方法,还有另一个弊端。由于函数调用是同步的(或者叫阻塞的),在消费者的方法没有返回之前,生产者只好一直等在那边。万一消费者处理数据很慢,生产者就会白白糟蹋大好时光。缓冲区还有另一个好处。如果制造数据的速度时快时慢,缓冲区的好处就体现出来了。当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。等生产者的制造速度慢下来,消费者再慢慢处理掉。
首先,其中有几个关键的名词概念:生产者 消费者 缓冲区 耦合 阻塞
生产者和消费者可能是两个方法,也可能是两个类,甚至也可以是两个系统。
如果只有这两个对象的话,一般模式如下:
这样虽然结构简单,但是存在弊端的。
一方面如果生产者一旦发生变化,或者异常,那么消费者将会受到致命影响——无法正常使用
另一方面如果生产者处理的时间过长,消费者需要等待的时间非常长。
所以引入缓冲区,即消息队列之后,如下
虽然,生产者和缓冲区,缓冲区和消费者之间还是存在耦合,但是生产者和消费者之间的耦合是降低了。
当任何一方发生变化,或者异常时,只需修改缓冲区即可。
而缓冲区的修改量是非常小的——缓冲区只是服务于相对应的生产者和消费者,修改逻辑不需要考虑到其它组件的影响。而如果不加入缓冲区,当某一方发生变化时,另一方不仅仅需要做相对应的修改,还需要考虑到其它组件的影响。这样将会像蝴蝶效应一样,往小了说影响着一部分工作,往大了说会影响着整个系统。
以及最后一句主要是讲关于缓冲区,即消息队列的另一个作用——削峰:
当数据生产过快的时候,消费者来不及处理,未处理的数据可以暂时存在缓存区中。等生产者的制造速度慢下来,消费者再慢慢处理
在前一篇 同步异步 已做分析。
简单地说,就是同步变异步后,业务响应的效率将会增加,不必等待前面的业务返回值后再继续
这里的峰,指的是峰值,更确切地说是流量峰值,或者请求峰值。例如以下场景:
当系统中的系统A的峰值为每秒处理1000个请求,平常使用是没有问题的。
但是当开展某个活动时,大量用户会在某个时刻发起请求,例如1000000个请求,甚至更多。
如果这些请求同时被处理的话,交易系统肯定承受不住这么高的压力。
然后,当这个活动结束,系统只有少量的用户在正常使用。
显然,且不说系统A运行时间的不平衡,是否能承受住正常处理能力的1000倍都是问题。
但是如果使用消息队列,将这些请求全部收集起来,然后以每秒1000个请求发送给系统A,甚至更少。
系统A是肯定能够处理的,那么将会在1000秒后处理完所有的请求,然后又恢复正常使用。
这期间,虽然消息队列会出现大量消息积压的情况,但系统A能够使用。
这就是消息队列常常被应用于秒杀系统等等活动的原因。
世上大概没有东西只有好处的吧,消息队列也是如此。
如果有,那也只能是因为你对它的认识不够了解。
以下的问题也是根据使用消息队列后的架构的每一个组件而言的。
当使用了消息队列以后,架构大概如下:
注:一个系统不只只承担生产者或者消费者一种职责,往往是这两种角色的混合体。
这是分布式服务本身的一个问题,但是放在消息队列中,更加严重。
虽然消息队列可以保证最终一致性,但最终一致性是在架构正常运行的条件下
当架构中间一环出现问题,例如:
在系统B运行异常的条件下,数据已在系统A完成逻辑,成为数据A。
但是当数据A到达系统B时,系统B发生异常,即使系统B之外的系统都是正常的,都不会得到正确最终结果
所以这是数据一致性问题
可以联想到事务的四大特性:原子性、一致性、隔离性、持久性
解决方案也在其中,为了保证一致性,可以把整个逻辑,即整个架构放在一个事务中,
更确切地说,将整个架构包装成一个原子,
要么一起成功,要么一起失败。
成功固然更好,如果失败的话,数据还是数据,不会在其它系统中发生作用。
之前说的解耦也是考虑到这个原因。但是当引入依赖过多,即引用了消息队列。
原先系统A到系统B,再到系统xxx的架构,加入了多个消息队列,就是如今的架构。
如果系统A到系统xxx都运行正常,消息队列发生异常,甚至是挂了,整个架构也就不能正常工作了。
系统的可用性就必须考虑到消息队列的因素。
“人生而自由,却无往不在枷锁之中。”
保证系统的高可用,这涉及到架构的设计上了,而不仅仅只是在消息队列层面。
看到一个评论,非常有启发
首先rabbitmq可以做集群,这样就保证了消息队列的可靠性,如果你的系统很大,如果不利用这种方式来解耦合,你的系统可靠性会很差,不能实现高可用,也就是会出现雪崩;
系统复杂性增加,这点也是片面的,你解除了系统的耦合,系统模块之间会变得更加的轻松,也是实现了分布式的原理。
同时消息是否被重用从两方面解决:1 你的系统整个业务是以状态机的形势来做,只有完成了上一步才能有下一步,利用数据的事务实现幂等,从而你消息可以无限的重复发送,但是事情我只做一次。
说到底就是你设计根据业务上要下功夫,任何东西都有它的使用场景,还有rabbitmq有多工作模式,不同的模式下面也是有着不同设置!
如果说上面两个问题是架构层面的,那消息队列层面的主要问题也有很多的。
使用消息队列,这些问题是必须考虑的——重复消费,信息丢失,消息顺序
因为需要考虑这些问题,系统的实现就会变得复杂。
当然,由于每个MQ中间件都是不一样的,所以需要根据MQ的特点和机制,使用针对性的更细节化的解决方案。
这个命题其实和**“如何保证消息消费时的幂等性”**是同一个概念。
幂等性:幂等是一个数学与计算机学概念,常见于抽象代数中。
在数学中,指的是以下公式:
符 合 公 式 : f ( x ) = f ( f ( x ) ) : − − − 一 元 运 算 中 , 都 是 符 合 的 , 即 : f ( x ) = 1 例 如 : f ( 1 ) = f ( f ( 1 ) ) − − − 二 元 运 算 中 , 只 有 0 和 1 是 符 合 的 , 即 : f ( x ) = x 2 例 如 : f ( 0 ) = 0 , f ( 1 ) = 1 符合公式:f(x) = f(f(x)):\\ ---\\ 一元运算中,都是符合的,即:\\ f(x) = 1\\ 例如:f(1) = f(f(1))\\ ---\\ 二元运算中,只有0和1是符合的,即:\\ f(x) = x^2\\ 例如:f(0) = 0,f(1) = 1 符合公式:f(x)=f(f(x)):−−−一元运算中,都是符合的,即:f(x)=1例如:f(1)=f(f(1))−−−二元运算中,只有0和1是符合的,即:f(x)=x2例如:f(0)=0,f(1)=1
在编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。例如数据库编程中的查询操作。
数学与编程中,幂等函数,幂等方法,指的是使用相同参数在相同的函数执行多次,函数产生的结果都是一样的。
在业务中,消息消费时的幂等,是非常重要的。
引用一个场景:有一篇博客,用户点击了一次收藏按钮。那么这条信息就发送到服务器,服务器再将这条数据持久化。如果收藏按钮未做相应处理实现锁定,当用户不断地点击收藏操作,那么会发生什么:
未考虑到信息的幂等性,那么用户不断点击,用户的收藏列表将会出现大量重复的博客。
考虑到信息的幂等性,将用户id和博客id进行绑定,每一次投票时,判断该用户是否收藏了该篇博客。收藏与否,做相应的处理。
幂等性体现在用户不断点击收藏,该用户的收藏列表中的这篇博客还是只有一篇。
根据上面的描述,可以得到2点结论:
保证消息队列的幂等性,在于开发者的实现
消息重复发送并不可怕,可怕的是消息重复发送后,所带来的后果。
例如重复查询并不可怕,但是重复插入就会导致冗余。
其实从上面的例子可以看到,保证消息队列的幂等性,在于开发者的实现。
而开发者需要考虑到业务的细节等等,并进行针对性处理。
一个简单的使用了消息队列的架构,主要如下:
可以看到,有3个角色,生产者,消息队列MQ,消费者
为了更方便理解,可以将这3个角色,理解成3个位置或者是地点。
因此,消息丢失也将发生于也是这3个位置
消息存在于生产者处时,将会被发送到消息队列。
当发送过程,由于网络传输不稳定或者某种原因,信息没有被发送到消息队列。
而生产者误以为消息正常发送到消息队列了。
消息丢失就发生了,而责任是在生产者处的——生产者误以为消息成功发送到消息队列
解决方案:如果生产者对信息是否到达消息队列做了处理以及确认,
当发送失败时,重新发送。那么消息就不会于生产者处被丢失。
如果生产者正确的完成它的职责,消息被发送到了MQ。
MQ需要将消息发送到消费者,如果消息没有完全被发送到消费者处,MQ宕机了。
此时消费者收到的消息不完整,消费者也没有开始消费。
而MQ宕机了,消息丢失了。下次重启时,MQ也不会重新发送这条消息了,那么这条消息就这样被丢失。
解决方案:需要在MQ处做好消息的持久化工作。当MQ宕机后重启,这条信息会从磁盘中被读取,再重新发送到消费者处。
当消息正常从生产者发送到MQ,再从MQ发送到消费者处后。
消费者开启消费工作,当没有消费完成时,就通知MQ已消费完成,MQ将会把这条信息设置为消费完成。
此时,消费者宕机了,消息丢失了。下次重启时,MQ不会再重新发送消息。
解析:MQ的工作是正常的,没有责任的。责任在于消费者——消息没有消费完成,就通知MQ已经完成消费。
解决方案:其实很简单,
当消费完成后,再发送信号到MQ,告诉MQ消息已经消费完成,不用再次发送了。
当消费未完成,消费者宕机后重启,MQ会重新发送消息到消费者处。
其实实现消息零丢失,也会导致性能大幅下降。
正如一个复杂逻辑往往比一个简单逻辑要慢,要难实现。
在刚学习消息队列时,看到一个例子——将消息队列比作邮局。那么生产者即寄信人,而消费者即收信人。
而丢失场景可以这样解释:
生产者处丢失:寄信人写好信之后,然后派信使把信送到邮局。而信使在途中挂掉了,信件被毁。邮局也不知道这个信使收到了什么人的信,寄信人也没有到邮局确认是否揽收到这封信。抛开跑腿的,责任在于寄信人,邮局没有责任。
如果,寄信人到邮局确认是否揽收到这封信,如果收到了,那么寄信人的工作就完成了。如果没有收到,则再写一封,让信使把信送到邮局。
MQ处丢失:寄信人成功把信送到了邮局,邮局成功收到这封信。然后在揽件时弄丢了信,又没有记录这个事故,那么这封信就这样丢失了。
如果,这个邮局有点特殊,每收到一封信,将会把这封信复制备份,并保密存储。当信被送到收信人手里时,再将这封备份信销毁;当信在派送到收信人的过程中,出现什么意外,再将这封备份的信派送。保证邮局肯定有一封备份的信,这样肯定不会出现信件丢失。
消费者处丢失:信件成功被邮局派送到收信人处,还没有拆开来看,就告诉邮局收到了信件。此时邮局马上就将这封信的备份销毁掉。然后收信人又不小心把这封信给弄丢(反正就是怎么都找不到的那种)了,那么信的内容将会永远看不到了。
如果,收信人不那么急反馈给邮局——收到了信件。等待把信的内容看完后,才告诉邮局收到了信件。
看完了还好。但是如果没有把信的内容看完,信就弄丢了,可以让邮局把邮局里那封备份的信送来。
当然,跳脱逻辑,回到现实中,信的存在并不会非常重要,但是如果把信封里的东西想象成磁带,或者手稿之类贵重东西的话,就非常符合了。
而现实中,邮局也不可能会把信件备份后存储,不仅仅是因为信件的不重要性,还因为隐私性。
还因为采取这套业务,邮局的运作的复杂性非常大,也会导致效率非常慢,例如备份信件等等工作。
这也是 实现消息零丢失 的弊端。
相对于上面2个模块,消息顺序并不是所有所有业务都需要考虑的问题。
例如,当消息队列都是多个同个等级的对象,消息是否按顺序执行,决定了这个请求什么时候可以完成。这也是不同用户使用相同应用程序,早提交请求的用户可能晚相应的问题出现。
这并不会影响业务的正常执行,因为时间差并不会太长,顶多几秒,情况再糟糕也是几分钟的问题。
需要担心的是:同一个对象不同的操作,例如以下场景:
如果不是按理想情况下发生,先删除将会发生空指针异常。
这种方式固然很好,但是如果高吞吐量,多消费者如何解决?
初看,其实非常像TCP的三次握手和四次挥手。
当消息队列1将消息“插入”到消费者1后,发送了信号2,消息队列2才会发送消息“更新”到消费者2。依次类推。
如果消息队列1发送了消息到消费者1处,消费者没有回复,即没有信号2。那么消息队列1是否应该再次将消息“插入”发送到消费者1呢?
应该再次发送。
首先,消费者1会有两种可能,消费完了和未消费。如果未消费了,那再次发送是可以的。
如果消费完了,未回复,再次发送就涉及到消息的重复消费了。
上面的3个问题在于开发过程就要考虑到的,关系着消息队列是否正常运作。
而这个问题就非常严重了,因为当大量消息堆积如果处理得不好的话,就很有可能丢失这些数据,将会是“开发事故”。
另外,处理消息堆积也是处理能力的体现。
消息堆积说白了就是MQ出现问题,那么原因往往在于消费消息的消费者上了。主要有以下几个原因:
- 生产者的生产速度与消费者的消费速度不匹配,即生产速度远远大于消费速度。
- 消息消费失败,反复重试
- 消费者消费能力弱
究其根本原因——消费者消费能力弱
排查:首先进行排查:有bug处理bug;如果逻辑过于冗余,则逻辑进行优化;
MQ:
“欲戴王冠,必先承其重”,在享受消息队列带来的好处的时候,就必须考虑——使用消息队列,必须解决的问题。
当然这些问题可以解决,但并不是百分百地,必然成功的。我们能够做的只是让这些问题的出现概率更小,甚至尽可能地不再出现。
那么,可不可以,避免这些问题的解决,而不使用呢。当然可以,
但是有些场景,如果不使用消息队列,就算完成了也是不合格的。
所以在某些场景下,就算再复杂,消息队列也是必须加入的方案。
其实生活中处处都有消息队列的影子,要学习看见。
一般大型公园都是售票点和检票点分开的。
试想一下,如果售票点和检票点设置在同一个点,当大量的游客到来时,每一个游客都需要支付买票的时间,这时间相对于检票而言,是非常久的。这将会导致公园门口有大量游客聚集在一起。
所以,如果采取售票点和检票点分开设置。游客可以先在售票点排队买票,然后,忙其它的事情,等待有空或者公园门口没那么多人的时候,再选择到检票点检票入内。
另外,这样设置,可以让售票点独立于检票点,将售票点设置与各处,例如互联网售票等等。
这是学习消息队列过程中的一些总结,如果有哪里理解错误,欢迎指出。