mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-20 08:03:43 +08:00
del
This commit is contained in:
127
极客时间专栏/geek/消息队列高手课/基础篇/01 | 为什么需要消息队列?.md
Normal file
127
极客时间专栏/geek/消息队列高手课/基础篇/01 | 为什么需要消息队列?.md
Normal file
@@ -0,0 +1,127 @@
|
||||
<audio id="audio" title="01 | 为什么需要消息队列?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fc/38/fc0e2c64317a6de05107cd2c6d6b3c38.mp3"></audio>
|
||||
|
||||
你好,我是李玥。今天我们来讲讲为什么需要消息队列,消息队列主要解决的是什么问题。
|
||||
|
||||
消息队列是最古老的中间件之一,从系统之间有通信需求开始,就自然产生了消息队列。但是给消息队列下一个准确的定义却不太容易。我们知道,消息队列的主要功能就是收发消息,但是它的作用不仅仅只是解决应用之间的通信问题这么简单。
|
||||
|
||||
我们举个例子说明一下消息队列的作用。话说小袁是一家巧克力作坊的老板,生产出美味的巧克力需要三道工序:首先将可可豆磨成可可粉,然后将可可粉加热并加入糖变成巧克力浆,最后将巧克力浆灌入模具,撒上坚果碎,冷却后就是成品巧克力了。
|
||||
|
||||
最开始的时候,每次研磨出一桶可可粉后,工人就会把这桶可可粉送到加工巧克力浆的工人手上,然后再回来加工下一桶可可粉。小袁很快就发现,其实工人可以不用自己运送半成品,于是他在每道工序之间都增加了一组传送带,研磨工人只要把研磨好的可可粉放到传送带上,就可以去加工下一桶可可粉了。 传送带解决了上下游工序之间的“通信”问题。
|
||||
|
||||
传送带上线后确实提高了生产效率,但也带来了新的问题:每道工序的生产速度并不相同。在巧克力浆车间,一桶可可粉传送过来时,工人可能正在加工上一批可可粉,没有时间接收。不同工序的工人们必须协调好什么时间往传送带上放置半成品,如果出现上下游工序加工速度不一致的情况,上下游工人之间必须互相等待,确保不会出现传送带上的半成品无人接收的情况。
|
||||
|
||||
为了解决这个问题,小袁在每组传送的下游带配备了一个暂存半成品的仓库,这样上游工人就不用等待下游工人有空,任何时间都可以把加工完成的半成品丢到传送带上,无法接收的货物被暂存在仓库中,下游工人可以随时来取。传送带配备的仓库实际上起到了“通信”过程中“缓存”的作用。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/84/ec/8476bca6176a7a11de452afca940feec.jpg" alt=""><br>
|
||||
传送带解决了半成品运输问题,仓库可以暂存一些半成品,解决了上下游生产速度不一致的问题,小袁在不知不觉中实现了一个巧克力工厂版的消息队列。
|
||||
|
||||
## 哪些问题适合使用消息队列来解决?
|
||||
|
||||
接下来我们说一下日常开发中,哪些问题适合使用消息队列解决。
|
||||
|
||||
#### 1. 异步处理
|
||||
|
||||
大多数程序员在面试中,应该都问过或被问过一个经典却没有标准答案的问题:如何设计一个秒杀系统?这个问题可以有一百个版本的合理答案,但大多数答案中都离不开消息队列。
|
||||
|
||||
秒杀系统需要解决的核心问题是,如何利用有限的服务器资源,尽可能多地处理短时间内的海量请求。我们知道,处理一个秒杀请求包含了很多步骤,例如:
|
||||
|
||||
- 风险控制;
|
||||
- 库存锁定;
|
||||
- 生成订单;
|
||||
- 短信通知;
|
||||
- 更新统计数据。
|
||||
|
||||
如果没有任何优化,正常的处理流程是:App将请求发送给网关,依次调用上述5个流程,然后将结果返回给APP。
|
||||
|
||||
对于对于这5个步骤来说,能否决定秒杀成功,实际上只有风险控制和库存锁定这2个步骤。只要用户的秒杀请求通过风险控制,并在服务端完成库存锁定,就可以给用户返回秒杀结果了,对于后续的生成订单、短信通知和更新统计数据等步骤,并不一定要在秒杀请求中处理完成。
|
||||
|
||||
所以当服务端完成前面2个步骤,确定本次请求的秒杀结果后,就可以马上给用户返回响应,然后把请求的数据放入消息队列中,由消息队列异步地进行后续的操作。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d2/be/d2c1ee3d4478580c0d2a8d80d0e833be.jpg" alt="">
|
||||
|
||||
处理一个秒杀请求,从5个步骤减少为2个步骤,这样不仅响应速度更快,并且在秒杀期间,我们可以把大量的服务器资源用来处理秒杀请求。秒杀结束后再把资源用于处理后面的步骤,充分利用有限的服务器资源处理更多的秒杀请求。
|
||||
|
||||
**可以看到,在这个场景中,消息队列被用于实现服务的异步处理。**这样做的好处是:
|
||||
|
||||
- 可以更快地返回结果;
|
||||
- 减少等待,自然实现了步骤之间的并发,提升系统总体的性能。
|
||||
|
||||
#### 2. 流量控制
|
||||
|
||||
继续说我们的秒杀系统,我们已经使用消息队列实现了部分工作的异步处理,但我们还面临一个问题:如何避免过多的请求压垮我们的秒杀系统?
|
||||
|
||||
一个设计健壮的程序有自我保护的能力,也就是说,它应该可以在海量的请求下,还能在自身能力范围内尽可能多地处理请求,拒绝处理不了的请求并且保证自身运行正常。不幸的是,现实中很多程序并没有那么“健壮”,而直接拒绝请求返回错误对于用户来说也是不怎么好的体验。
|
||||
|
||||
因此,我们需要设计一套足够健壮的架构来将后端的服务保护起来。**我们的设计思路是,使用消息队列隔离网关和后端服务,以达到流量控制和保护后端服务的目的。**
|
||||
|
||||
加入消息队列后,整个秒杀流程变为:
|
||||
|
||||
1. 网关在收到请求后,将请求放入请求消息队列;
|
||||
1. 后端服务从请求消息队列中获取APP请求,完成后续秒杀处理过程,然后返回结果。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/79/4a/7909fb792a059e22a0a269c1f2cde64a.jpg" alt="">
|
||||
|
||||
秒杀开始后,当短时间内大量的秒杀请求到达网关时,不会直接冲击到后端的秒杀服务,而是先堆积在消息队列中,后端服务按照自己的最大处理能力,从消息队列中消费请求进行处理。
|
||||
|
||||
对于超时的请求可以直接丢弃,APP将超时无响应的请求处理为秒杀失败即可。运维人员还可以随时增加秒杀服务的实例数量进行水平扩容,而不用对系统的其他部分做任何更改。
|
||||
|
||||
这种设计的优点是:能根据下游的处理能力自动调节流量,达到“削峰填谷”的作用。但这样做同样是有代价的:
|
||||
|
||||
- 增加了系统调用链环节,导致总体的响应时延变长。
|
||||
- 上下游系统都要将同步调用改为异步消息,增加了系统的复杂度。
|
||||
|
||||
那还有没有更简单一点儿的流量控制方法呢?如果我们能预估出秒杀服务的处理能力,就可以用消息队列实现一个令牌桶,更简单地进行流量控制。
|
||||
|
||||
令牌桶控制流量的原理是:单位时间内只发放固定数量的令牌到令牌桶中,规定服务在处理请求之前必须先从令牌桶中拿出一个令牌,如果令牌桶中没有令牌,则拒绝请求。这样就保证单位时间内,能处理的请求不超过发放令牌的数量,起到了流量控制的作用。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2c/89/2c4e42056b78fff7746de28245910f89.jpg" alt="">
|
||||
|
||||
实现的方式也很简单,不需要破坏原有的调用链,只要网关在处理APP请求时增加一个获取令牌的逻辑。
|
||||
|
||||
令牌桶可以简单地用一个有固定容量的消息队列加一个“令牌发生器”来实现:令牌发生器按照预估的处理能力,匀速生产令牌并放入令牌队列(如果队列满了则丢弃令牌),网关在收到请求时去令牌队列消费一个令牌,获取到令牌则继续调用后端秒杀服务,如果获取不到令牌则直接返回秒杀失败。
|
||||
|
||||
以上是常用的使用消息队列两种进行流量控制的设计方法,你可以根据各自的优缺点和不同的适用场景进行合理选择。
|
||||
|
||||
#### 3. 服务解耦
|
||||
|
||||
消息队列的另外一个作用,就是实现系统应用之间的解耦。再举一个电商的例子来说明解耦的作用和必要性。
|
||||
|
||||
我们知道订单是电商系统中比较核心的数据,当一个新订单创建时:
|
||||
|
||||
1. 支付系统需要发起支付流程;
|
||||
1. 风控系统需要审核订单的合法性;
|
||||
1. 客服系统需要给用户发短信告知用户;
|
||||
1. 经营分析系统需要更新统计数据;
|
||||
1. ……
|
||||
|
||||
这些订单下游的系统都需要实时获得订单数据。随着业务不断发展,这些订单下游系统不断的增加,不断变化,并且每个系统可能只需要订单数据的一个子集,负责订单服务的开发团队不得不花费很大的精力,应对不断增加变化的下游系统,不停地修改调试订单系统与这些下游系统的接口。任何一个下游系统接口变更,都需要订单模块重新进行一次上线,对于一个电商的核心服务来说,这几乎是不可接受的。
|
||||
|
||||
所有的电商都选择用消息队列来解决类似的系统耦合过于紧密的问题。引入消息队列后,订单服务在订单变化时发送一条消息到消息队列的一个主题Order中,所有下游系统都订阅主题Order,这样每个下游系统都可以获得一份实时完整的订单数据。
|
||||
|
||||
无论增加、减少下游系统或是下游系统需求如何变化,订单服务都无需做任何更改,实现了订单服务与下游服务的解耦。
|
||||
|
||||
## 小结
|
||||
|
||||
以上就是消息队列最常被使用的三种场景:异步处理、流量控制和服务解耦。当然,消息队列的适用范围不仅仅局限于这些场景,还有包括:
|
||||
|
||||
- 作为发布/订阅系统实现一个微服务级系统间的观察者模式;
|
||||
- 连接流计算任务和数据;
|
||||
- 用于将消息广播给大量接收者。
|
||||
|
||||
简单的说,我们在单体应用里面需要用队列解决的问题,在分布式系统中大多都可以用消息队列来解决。
|
||||
|
||||
同时我们也要认识到,消息队列也有它自身的一些问题和局限性,包括:
|
||||
|
||||
- 引入消息队列带来的延迟问题;
|
||||
- 增加了系统的复杂度;
|
||||
- 可能产生数据不一致的问题。
|
||||
|
||||
所以我们说没有最好的架构,只有最适合的架构,根据目标业务的特点和自身条件选择合适的架构,才是体现一个架构师功力的地方。
|
||||
|
||||
## 思考题
|
||||
|
||||
在你工作或学习涉及到的系统中,哪些问题可以通过引入消息队列来解决?对于系统中已经使用消息队列,可以对应到这一讲中提到的哪个场景?如果没有可以对应的场景,那这个消息队列在系统中起到的是什么作用?解决了什么问题?是否又带来了什么新的问题?欢迎在留言区写下你的想法。
|
||||
|
||||
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||
|
||||
|
||||
115
极客时间专栏/geek/消息队列高手课/基础篇/02 | 该如何选择消息队列?.md
Normal file
115
极客时间专栏/geek/消息队列高手课/基础篇/02 | 该如何选择消息队列?.md
Normal file
@@ -0,0 +1,115 @@
|
||||
<audio id="audio" title="02 | 该如何选择消息队列?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c2/61/c2131cac5c98e1a4151a9e1fb46fb961.mp3"></audio>
|
||||
|
||||
你好,我是李玥。这节课我们来聊一下几个比较常见的开源的消息队列中间件。如果你正在做消息队列技术选型,不知道该选择哪款消息队列,你一定要先听一下这节课的内容。
|
||||
|
||||
作为一个程序员,相信你一定听过“没有银弹”这个说法,这里面的银弹是指能轻松杀死狼人、用白银做的子弹,什么意思呢?我对这句话的理解是说,在软件工程中,不存在像“银弹”这样可以解决一切问题的设计、架构或软件,每一个软件系统,它都是独一无二的,你不可能用一套方法去解决所有的问题。
|
||||
|
||||
在消息队列的技术选型这个问题上,也是同样的道理。并不存在说,哪个消息队列就是“最好的”。常用的这几个消息队列,每一个产品都有自己的优势和劣势,你需要根据现有系统的情况,选择最适合你的那款产品。
|
||||
|
||||
## 选择消息队列产品的基本标准
|
||||
|
||||
虽然这些消息队列产品在功能和特性方面各有优劣,但我们在选择的时候要有一个最低标准,保证入选的产品至少是及格的。
|
||||
|
||||
接下来我们先说一下这及格的标准是什么样的。
|
||||
|
||||
首先,必须是开源的产品,这个非常重要。开源意味着,如果有一天你使用的消息队列遇到了一个影响你系统业务的Bug,你至少还有机会通过修改源代码来迅速修复或规避这个Bug,解决你的系统火烧眉毛的问题,而不是束手无策地等待开发者不一定什么时候发布的下一个版本来解决。
|
||||
|
||||
其次,这个产品必须是近年来比较流行并且有一定社区活跃度的产品。流行的好处是,只要你的使用场景不太冷门,你遇到Bug的概率会非常低,因为大部分你可能遇到的Bug,其他人早就遇到并且修复了。你在使用过程中遇到的一些问题,也比较容易在网上搜索到类似的问题,然后很快的找到解决方案。
|
||||
|
||||
还有一个优势就是,流行的产品与周边生态系统会有一个比较好的集成和兼容,比如,Kafka和Flink就有比较好的兼容性,Flink内置了Kafka的Data Source,使用Kafka就很容易作为Flink的数据源开发流计算应用,如果你用一个比较小众的消息队列产品,在进行流计算的时候,你就不得不自己开发一个Flink的Data Source。
|
||||
|
||||
最后,作为一款及格的消息队列产品,必须具备的几个特性包括:
|
||||
|
||||
- 消息的可靠传递:确保不丢消息;
|
||||
- Cluster:支持集群,确保不会因为某个节点宕机导致服务不可用,当然也不能丢消息;
|
||||
- 性能:具备足够好的性能,能满足绝大多数场景的性能要求。
|
||||
|
||||
接下来我们一起看一下有哪些符合上面这些条件,可供选择的开源消息队列产品。
|
||||
|
||||
## 可供选择的消息队列产品
|
||||
|
||||
**1. RabbitMQ**
|
||||
|
||||
首先,我们说一下老牌儿消息队列RabbitMQ,俗称兔子MQ。RabbitMQ是使用一种比较小众的编程语言:Erlang语言编写的,它最早是为电信行业系统之间的可靠通信设计的,也是少数几个支持AMQP协议的消息队列之一。
|
||||
|
||||
RabbitMQ就像它的名字中的兔子一样:轻量级、迅捷,它的Slogan,也就是宣传口号,也很明确地表明了RabbitMQ的特点:Messaging that just works,“开箱即用的消息队列”。也就是说,RabbitMQ是一个相当轻量级的消息队列,非常容易部署和使用。
|
||||
|
||||
另外RabbitMQ还号称是世界上使用最广泛的开源消息队列,是不是真的使用率世界第一,我们没有办法统计,但至少是“最流行的消息中间之一”,这是没有问题的。
|
||||
|
||||
RabbitMQ一个比较有特色的功能是支持非常灵活的路由配置,和其他消息队列不同的是,它在生产者(Producer)和队列(Queue)之间增加了一个Exchange模块,你可以理解为交换机。
|
||||
|
||||
这个Exchange模块的作用和交换机也非常相似,根据配置的路由规则将生产者发出的消息分发到不同的队列中。路由的规则也非常灵活,甚至你可以自己来实现路由规则。基于这个Exchange,可以产生很多的玩儿法,如果你正好需要这个功能,RabbitMQ是个不错的选择。
|
||||
|
||||
RabbitMQ的客户端支持的编程语言大概是所有消息队列中最多的,如果你的系统是用某种冷门语言开发的,那你多半可以找到对应的RabbitMQ客户端。
|
||||
|
||||
接下来说下RabbitMQ的几个问题。
|
||||
|
||||
第一个问题是,RabbitMQ对消息堆积的支持并不好,在它的设计理念里面,消息队列是一个管道,大量的消息积压是一种不正常的情况,应当尽量去避免。当大量消息积压的时候,会导致RabbitMQ的性能急剧下降。
|
||||
|
||||
第二个问题是,RabbitMQ的性能是我们介绍的这几个消息队列中最差的,根据官方给出的测试数据综合我们日常使用的经验,依据硬件配置的不同,它大概每秒钟可以处理几万到十几万条消息。其实,这个性能也足够支撑绝大多数的应用场景了,不过,如果你的应用对消息队列的性能要求非常高,那不要选择RabbitMQ。
|
||||
|
||||
最后一个问题是RabbitMQ使用的编程语言Erlang,这个编程语言不仅是非常小众的语言,更麻烦的是,这个语言的学习曲线非常陡峭。大多数流行的编程语言,比如Java、C/C++、Python和JavaScript,虽然语法、特性有很多的不同,但它们基本的体系结构都是一样的,你只精通一种语言,也很容易学习其他的语言,短时间内即使做不到精通,但至少能达到“会用”的水平。
|
||||
|
||||
就像一个以英语为母语的人,学习法语、德语都很容易,但是你要是让他去学汉语,那基本上和学习其他这些语言不是一个难度级别的。很不幸的是,Erlang就是编程语言中的“汉语”。所以如果你想基于RabbitMQ做一些扩展和二次开发什么的,建议你慎重考虑一下可持续维护的问题。
|
||||
|
||||
**2. RocketMQ**
|
||||
|
||||
RocketMQ是阿里巴巴在2012年开源的消息队列产品,后来捐赠给 Apache 软件基金会,2017正式毕业,成为Apache的顶级项目。阿里内部也是使用RocketMQ作为支撑其业务的消息队列,经历过多次“双十一”考验,它的性能、稳定性和可靠性都是值得信赖的。作为优秀的国产消息队列,近年来越来越多的被国内众多大厂使用。
|
||||
|
||||
我在总结RocketMQ的特点时,发现很难找出RocketMQ有什么特别让我印象深刻的特点,也很难找到它有什么缺点。
|
||||
|
||||
RocketMQ就像一个品学兼优的好学生,有着不错的性能,稳定性和可靠性,具备一个现代的消息队列应该有的几乎全部功能和特性,并且它还在持续的成长中。
|
||||
|
||||
RocketMQ有非常活跃的中文社区,大多数问题你都可以找到中文的答案,也许会成为你选择它的一个原因。另外,RocketMQ使用Java语言开发,它的贡献者大多数都是中国人,源代码相对也比较容易读懂,你很容易对RocketMQ进行扩展或者二次开发。
|
||||
|
||||
RocketMQ对在线业务的响应时延做了很多的优化,大多数情况下可以做到毫秒级的响应,**如果你的应用场景很在意响应时延,那应该选择使用RocketMQ。**
|
||||
|
||||
RocketMQ的性能比RabbitMQ要高一个数量级,每秒钟大概能处理几十万条消息。
|
||||
|
||||
RocketMQ的一个劣势是,作为国产的消息队列,相比国外的比较流行的同类产品,在国际上还没有那么流行,与周边生态系统的集成和兼容程度要略逊一筹。
|
||||
|
||||
**3. Kafka**
|
||||
|
||||
最后我们聊一聊Kafka。Kafka最早是由LinkedIn开发,目前也是Apache的顶级项目。Kafka最初的设计目的是用于处理海量的日志。
|
||||
|
||||
在早期的版本中,为了获得极致的性能,在设计方面做了很多的牺牲,比如不保证消息的可靠性,可能会丢失消息,也不支持集群,功能上也比较简陋,这些牺牲对于处理海量日志这个特定的场景都是可以接受的。这个时期的Kafka甚至不能称之为一个合格的消息队列。
|
||||
|
||||
但是,请注意,重点一般都在后面。随后的几年Kafka逐步补齐了这些短板,你在网上搜到的很多消息队列的对比文章还在说Kafka不可靠,其实这种说法早已经过时了。当下的Kafka已经发展为一个非常成熟的消息队列产品,无论在数据可靠性、稳定性和功能特性等方面都可以满足绝大多数场景的需求。
|
||||
|
||||
**Kafka与周边生态系统的兼容性是最好的没有之一,尤其在大数据和流计算领域,几乎所有的相关开源软件系统都会优先支持Kafka。**
|
||||
|
||||
Kafka使用Scala和Java语言开发,设计上大量使用了批量和异步的思想,这种设计使得Kafka能做到超高的性能。Kafka的性能,尤其是异步收发的性能,是三者中最好的,但与RocketMQ并没有量级上的差异,大约每秒钟可以处理几十万条消息。
|
||||
|
||||
我曾经使用配置比较好的服务器对Kafka进行过压测,在有足够的客户端并发进行异步批量发送,并且开启压缩的情况下,Kafka的极限处理能力可以超过每秒2000万条消息。
|
||||
|
||||
但是Kafka这种异步批量的设计带来的问题是,它的同步收发消息的响应时延比较高,因为当客户端发送一条消息的时候,Kafka并不会立即发送出去,而是要等一会儿攒一批再发送,在它的Broker中,很多地方都会使用这种“先攒一波再一起处理”的设计。当你的业务场景中,每秒钟消息数量没有那么多的时候,Kafka的时延反而会比较高。所以,**Kafka不太适合在线业务场景。**
|
||||
|
||||
## 第二梯队的消息队列
|
||||
|
||||
除了上面给你介绍的三大消息队列之外,还有几个第二梯队的产品,我个人的观点是,这些产品之所以没那么流行,或多或少都有着比较明显的短板,不推荐使用。在这儿呢,我简单介绍一下,纯当丰富你的知识广度。
|
||||
|
||||
先说ActiveMQ,ActiveMQ是最老牌的开源消息队列,是十年前唯一可供选择的开源消息队列,目前已进入老年期,社区不活跃。无论是功能还是性能方面,ActiveMQ都与现代的消息队列存在明显的差距,它存在的意义仅限于兼容那些还在用的爷爷辈儿的系统。
|
||||
|
||||
接下来说说ZeroMQ,严格来说ZeroMQ并不能称之为一个消息队列,而是一个基于消息队列的多线程网络库,如果你的需求是将消息队列的功能集成到你的系统进程中,可以考虑使用ZeroMQ。
|
||||
|
||||
最后说一下Pulsar,很多人可能都没听说过这个产品,Pulsar是一个新兴的开源消息队列产品,最早是由Yahoo开发,目前处于成长期,流行度和成熟度相对没有那么高。与其他消息队列最大的不同是,Pulsar采用存储和计算分离的设计,我个人非常喜欢这种设计,它有可能会引领未来消息队列的一个发展方向,建议你持续关注这个项目。
|
||||
|
||||
## 总结
|
||||
|
||||
在了解了上面这些开源消息队列各自的特点和优劣势后,我相信你对于消息队列的选择已经可以做到心中有数了。我也总结了几条选择的建议供你参考。
|
||||
|
||||
如果说,消息队列并不是你将要构建系统的主角之一,你对消息队列功能和性能都没有很高的要求,只需要一个开箱即用易于维护的产品,我建议你使用RabbitMQ。
|
||||
|
||||
如果你的系统使用消息队列主要场景是处理在线业务,比如在交易系统中用消息队列传递订单,那RocketMQ的低延迟和金融级的稳定性是你需要的。
|
||||
|
||||
如果你需要处理海量的消息,像收集日志、监控信息或是前端的埋点这类数据,或是你的应用场景大量使用了大数据、流计算相关的开源产品,那Kafka是最适合你的消息队列。
|
||||
|
||||
如果我说的这些场景和你的场景都不符合,你看了我之前介绍的这些消息队列的特点后,还是不知道如何选择,那就选你最熟悉的吧,毕竟这些产品都能满足大多数应用场景,使用熟悉的产品还可以快速上手不是?
|
||||
|
||||
## 思考题
|
||||
|
||||
本节课的思考题也是围绕着消息队列的技术选型来设置的。你开发过的或是正在开发的系统,对消息队列的需求是什么样的?现在选择的消息队列是哪款产品?在学完了本节课后,你觉得当前选择的消息队列是否是最佳的选择?理由是什么?欢迎在留言区与我分享讨论。
|
||||
|
||||
感谢阅读,如果你觉得这篇文章对你有一些启发,也欢迎把它分享给你的朋友。
|
||||
|
||||
|
||||
103
极客时间专栏/geek/消息队列高手课/基础篇/03 | 消息模型:主题和队列有什么区别?.md
Normal file
103
极客时间专栏/geek/消息队列高手课/基础篇/03 | 消息模型:主题和队列有什么区别?.md
Normal file
@@ -0,0 +1,103 @@
|
||||
<audio id="audio" title="03 | 消息模型:主题和队列有什么区别?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d4/fd/d42bd3e7555aabe4fa56c2e21fe029fd.mp3"></audio>
|
||||
|
||||
你好,我是李玥。这节课我们来学习消息队列中像队列、主题、分区等基础概念。这些基础的概念,就像我们学习一门编程语言中的基础语法一样,你只有搞清楚它们,才能进行后续的学习。
|
||||
|
||||
如果你研究过超过一种消息队列产品,你可能已经发现,每种消息队列都有自己的一套消息模型,像队列(Queue)、主题(Topic)或是分区(Partition)这些名词概念,在每个消息队列模型中都会涉及一些,含义还不太一样。
|
||||
|
||||
为什么出现这种情况呢?因为没有标准。曾经,也是有一些国际组织尝试制定过消息相关的标准,比如早期的JMS和AMQP。但让人无奈的是,标准的进化跟不上消息队列的演进速度,这些标准实际上已经被废弃了。
|
||||
|
||||
那么,到底什么是队列?什么是主题?主题和队列又有什么区别呢?想要彻底理解这些,我们需要从消息队列的演进说起。
|
||||
|
||||
## 主题和队列有什么区别?
|
||||
|
||||
在互联网的架构师圈儿中间,流传着这样一句不知道出处的名言,我非常认同和喜欢:好的架构不是设计出来的,而是演进出来的。 现代的消息队列呈现出的模式,一样是经过之前的十几年逐步演进而来的。
|
||||
|
||||
最初的消息队列,就是一个严格意义上的队列。在计算机领域,“队列(Queue)”是一种数据结构,有完整而严格的定义。在维基百科中,队列的定义是这样的:
|
||||
|
||||
>
|
||||
队列是先进先出(FIFO, First-In-First-Out)的线性表(Linear List)。在具体应用中通常用链表或者数组来实现。队列只允许在后端(称为rear)进行插入操作,在前端(称为front)进行删除操作。
|
||||
|
||||
|
||||
这个定义里面包含几个关键点,第一个是先进先出,这里面隐含着的一个要求是,在消息入队出队过程中,需要保证这些消息**严格有序**,按照什么顺序写进队列,必须按照同样的顺序从队列中读出来。不过,队列是没有“读”这个操作的,“读”就是出队,也就是从队列中“删除”这条消息。
|
||||
|
||||
**早期的消息队列,就是按照“队列”的数据结构来设计的。**我们一起看下这个图,生产者(Producer)发消息就是入队操作,消费者(Consumer)收消息就是出队也就是删除操作,服务端存放消息的容器自然就称为“队列”。
|
||||
|
||||
这就是最初的一种消息模型:队列模型。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b1/84/b18f43f67ae1b0d24d88f0ba39708a84.jpg" alt="">
|
||||
|
||||
如果有多个生产者往同一个队列里面发送消息,这个队列中可以消费到的消息,就是这些生产者生产的所有消息的合集。消息的顺序就是这些生产者发送消息的自然顺序。如果有多个消费者接收同一个队列的消息,这些消费者之间实际上是竞争的关系,每个消费者只能收到队列中的一部分消息,也就是说任何一条消息只能被其中的一个消费者收到。
|
||||
|
||||
如果需要将一份消息数据分发给多个消费者,要求每个消费者都能收到全量的消息,例如,对于一份订单数据,风控系统、分析系统、支付系统等都需要接收消息。这个时候,单个队列就满足不了需求,一个可行的解决方式是,为每个消费者创建一个单独的队列,让生产者发送多份。
|
||||
|
||||
显然这是个比较蠢的做法,同样的一份消息数据被复制到多个队列中会浪费资源,更重要的是,生产者必须知道有多少个消费者。为每个消费者单独发送一份消息,这实际上违背了消息队列“解耦”这个设计初衷。
|
||||
|
||||
为了解决这个问题,演化出了另外一种消息模型:“**发布-订阅模型(Publish-Subscribe Pattern)**”。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d5/54/d5c0742113b2a6f5a419e1ffc3327354.jpg" alt="">
|
||||
|
||||
在发布-订阅模型中,消息的发送方称为发布者(Publisher),消息的接收方称为订阅者(Subscriber),服务端存放消息的容器称为主题(Topic)。发布者将消息发送到主题中,订阅者在接收消息之前需要先“订阅主题”。“订阅”在这里既是一个动作,同时还可以认为是主题在消费时的一个逻辑副本,每份订阅中,订阅者都可以接收到主题的所有消息。
|
||||
|
||||
在消息领域的历史上很长的一段时间,队列模式和发布-订阅模式是并存的,有些消息队列同时支持这两种消息模型,比如ActiveMQ。我们仔细对比一下这两种模型,生产者就是发布者,消费者就是订阅者,队列就是主题,并没有本质的区别。**它们最大的区别其实就是,一份消息数据能不能被消费多次的问题。**
|
||||
|
||||
实际上,在这种发布-订阅模型中,如果只有一个订阅者,那它和队列模型就基本是一样的了。也就是说,发布-订阅模型在功能层面上是可以兼容队列模型的。
|
||||
|
||||
现代的消息队列产品使用的消息模型大多是这种发布-订阅模型,当然也有例外。
|
||||
|
||||
## RabbitMQ的消息模型
|
||||
|
||||
这个例外就是RabbitMQ,它是少数依然坚持使用队列模型的产品之一。那它是怎么解决多个消费者的问题呢?你还记得我在上节课中讲到RabbitMQ的一个特色Exchange模块吗?在RabbitMQ中,Exchange位于生产者和队列之间,生产者并不关心将消息发送给哪个队列,而是将消息发送给Exchange,由Exchange上配置的策略来决定将消息投递到哪些队列中。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2d/a5/2df04ce80ff54702240df8598f277ca5.jpg" alt="">
|
||||
|
||||
同一份消息如果需要被多个消费者来消费,需要配置Exchange将消息发送到多个队列,每个队列中都存放一份完整的消息数据,可以为一个消费者提供消费服务。这也可以变相地实现新发布-订阅模型中,“一份消息数据可以被多个订阅者来多次消费”这样的功能。具体的配置你可以参考RabbitMQ官方教程,其中一个[章节](http://www.rabbitmq.com/tutorials/tutorial-three-python.html)专门是讲如何实现发布订阅的。
|
||||
|
||||
## RocketMQ的消息模型
|
||||
|
||||
讲完了RabbitMQ的消息模型,我们再来看看RocketMQ。RocketMQ使用的消息模型是标准的发布-订阅模型,在RocketMQ的术语表中,生产者、消费者和主题与我在上面讲的发布-订阅模型中的概念是完全一样的。
|
||||
|
||||
但是,在RocketMQ也有队列(Queue)这个概念,并且队列在RocketMQ中是一个非常重要的概念,那队列在RocketMQ中的作用是什么呢?这就要从消息队列的消费机制说起。
|
||||
|
||||
几乎所有的消息队列产品都使用一种非常朴素的“请求-确认”机制,确保消息不会在传递过程中由于网络或服务器故障丢失。具体的做法也非常简单。在生产端,生产者先将消息发送给服务端,也就是Broker,服务端在收到消息并将消息写入主题或者队列中后,会给生产者发送确认的响应。
|
||||
|
||||
如果生产者没有收到服务端的确认或者收到失败的响应,则会重新发送消息;在消费端,消费者在收到消息并完成自己的消费业务逻辑(比如,将数据保存到数据库中)后,也会给服务端发送消费成功的确认,服务端只有收到消费确认后,才认为一条消息被成功消费,否则它会给消费者重新发送这条消息,直到收到对应的消费成功确认。
|
||||
|
||||
这个确认机制很好地保证了消息传递过程中的可靠性,但是,引入这个机制在消费端带来了一个不小的问题。什么问题呢?为了确保消息的有序性,在某一条消息被成功消费之前,下一条消息是不能被消费的,否则就会出现消息空洞,违背了有序性这个原则。
|
||||
|
||||
也就是说,每个主题在任意时刻,至多只能有一个消费者实例在进行消费,那就没法通过水平扩展消费者的数量来提升消费端总体的消费性能。为了解决这个问题,RocketMQ在主题下面增加了队列的概念。
|
||||
|
||||
**每个主题包含多个队列,通过多个队列来实现多实例并行生产和消费。**需要注意的是,RocketMQ只在队列上保证消息的有序性,主题层面是无法保证消息的严格顺序的。
|
||||
|
||||
RocketMQ中,订阅者的概念是通过消费组(Consumer Group)来体现的。每个消费组都消费主题中一份完整的消息,不同消费组之间消费进度彼此不受影响,也就是说,一条消息被Consumer Group1消费过,也会再给Consumer Group2消费。
|
||||
|
||||
消费组中包含多个消费者,同一个组内的消费者是竞争消费的关系,每个消费者负责消费组内的一部分消息。如果一条消息被消费者Consumer1消费了,那同组的其他消费者就不会再收到这条消息。
|
||||
|
||||
在Topic的消费过程中,由于消息需要被不同的组进行多次消费,所以消费完的消息并不会立即被删除,这就需要RocketMQ为每个消费组在每个队列上维护一个消费位置(Consumer Offset),这个位置之前的消息都被消费过,之后的消息都没有被消费过,每成功消费一条消息,消费位置就加一。这个消费位置是非常重要的概念,我们在使用消息队列的时候,丢消息的原因大多是由于消费位置处理不当导致的。
|
||||
|
||||
RocketMQ的消息模型中,比较关键的概念就是这些了。为了便于你理解,我画了下面这张图:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/46/17/465142ab5b5096f283118c307e8cc117.jpg" alt="">
|
||||
|
||||
你可以对照这张图再把我刚刚讲的这些概念继续消化一下,加深理解。
|
||||
|
||||
## Kafka的消息模型
|
||||
|
||||
我们再来看看另一种常见的消息队列Kafka,Kafka的消息模型和RocketMQ是完全一样的,我刚刚讲的所有RocketMQ中对应的概念,和生产消费过程中的确认机制,都完全适用于Kafka。唯一的区别是,在Kafka中,队列这个概念的名称不一样,Kafka中对应的名称是“分区(Partition)”,含义和功能是没有任何区别的。
|
||||
|
||||
## 小结
|
||||
|
||||
我们来总结一下本节课学习的内容。首先我们讲了队列和主题的区别,这两个概念的背后实际上对应着两种不同的消息模型:队列模型和发布-订阅模型。然后你需要理解,这两种消息模型其实并没有本质上的区别,都可以通过一些扩展或者变化来互相替代。
|
||||
|
||||
常用的消息队列中,RabbitMQ采用的是队列模型,但是它一样可以实现发布-订阅的功能。RocketMQ和Kafka采用的是发布-订阅模型,并且二者的消息模型是基本一致的。
|
||||
|
||||
最后提醒你一点,我这节课讲的消息模型和相关的概念是业务层面的模型,深刻理解业务模型有助于你用最佳的姿势去使用消息队列。
|
||||
|
||||
但业务模型不等于就是实现层面的模型。比如说MySQL和Hbase同样是支持SQL的数据库,它们的业务模型中,存放数据的单元都是“表”,但是在实现层面,没有哪个数据库是以二维表的方式去存储数据的,MySQL使用B+树来存储数据,而HBase使用的是KV的结构来存储。同样,像Kafka和RocketMQ的业务模型基本是一样的,并不是说他们的实现就是一样的,实际上这两个消息队列的实现是完全不同的。
|
||||
|
||||
## 思考题
|
||||
|
||||
最后给大家留一个思考题。刚刚我在介绍RocketMQ的消息模型时讲过,在消费的时候,为了保证消息的不丢失和严格顺序,每个队列只能串行消费,无法做到并发,否则会出现消费空洞的问题。那如果放宽一下限制,不要求严格顺序,能否做到单个队列的并行消费呢?如果可以,该如何实现?欢迎在留言区与我分享讨论。
|
||||
|
||||
感谢阅读,如果你觉得这篇文章对你有一些启发,也欢迎把它分享给你的朋友。
|
||||
|
||||
|
||||
109
极客时间专栏/geek/消息队列高手课/基础篇/04 | 如何利用事务消息实现分布式事务?.md
Normal file
109
极客时间专栏/geek/消息队列高手课/基础篇/04 | 如何利用事务消息实现分布式事务?.md
Normal file
@@ -0,0 +1,109 @@
|
||||
<audio id="audio" title="04 | 如何利用事务消息实现分布式事务?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4b/2d/4b9b66249362e1ea2982efc5a37fec2d.mp3"></audio>
|
||||
|
||||
你好,我是李玥,今天我们来聊一聊消息和事务。
|
||||
|
||||
一说起事务,你可能自然会联想到数据库。的确,我们日常使用事务的场景,绝大部分都是在操作数据库的时候。像MySQL、Oracle这些主流的关系型数据库,也都提供了完整的事务实现。那消息队列为什么也需要事务呢?
|
||||
|
||||
其实很多场景下,我们“发消息”这个过程,目的往往是通知另外一个系统或者模块去更新数据,**消息队列中的“事务”,主要解决的是消息生产者和消息消费者的数据一致性问题。**
|
||||
|
||||
依然拿我们熟悉的电商来举个例子。一般来说,用户在电商APP上购物时,先把商品加到购物车里,然后几件商品一起下单,最后支付,完成购物流程,就可以愉快地等待收货了。
|
||||
|
||||
这个过程中有一个需要用到消息队列的步骤,订单系统创建订单后,发消息给购物车系统,将已下单的商品从购物车中删除。因为从购物车删除已下单商品这个步骤,并不是用户下单支付这个主要流程中必需的步骤,使用消息队列来异步清理购物车是更加合理的设计。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d6/00/d6efbd1a48cb0d1cd352587f233c2500.jpg" alt="">
|
||||
|
||||
对于订单系统来说,它创建订单的过程中实际上执行了2个步骤的操作:
|
||||
|
||||
1. 在订单库中插入一条订单数据,创建订单;
|
||||
1. 发消息给消息队列,消息的内容就是刚刚创建的订单。
|
||||
|
||||
购物车系统订阅相应的主题,接收订单创建的消息,然后清理购物车,在购物车中删除订单中的商品。
|
||||
|
||||
在分布式系统中,上面提到的这些步骤,任何一个步骤都有可能失败,如果不做任何处理,那就有可能出现订单数据与购物车数据不一致的情况,比如说:
|
||||
|
||||
- 创建了订单,没有清理购物车;
|
||||
- 订单没创建成功,购物车里面的商品却被清掉了。
|
||||
|
||||
那我们需要解决的问题可以总结为:在上述任意步骤都有可能失败的情况下,还要保证订单库和购物车库这两个库的数据一致性。
|
||||
|
||||
对于购物车系统收到订单创建成功消息清理购物车这个操作来说,失败的处理比较简单,只要成功执行购物车清理后再提交消费确认即可,如果失败,由于没有提交消费确认,消息队列会自动重试。
|
||||
|
||||
问题的关键点集中在订单系统,创建订单和发送消息这两个步骤要么都操作成功,要么都操作失败,不允许一个成功而另一个失败的情况出现。
|
||||
|
||||
这就是事务需要解决的问题。
|
||||
|
||||
## 什么是分布式事务?
|
||||
|
||||
那什么是事务呢?如果我们需要对若干数据进行更新操作,为了保证这些数据的完整性和一致性,我们希望这些更新操作**要么都成功,要么都失败。**至于更新的数据,不只局限于数据库中的数据,可以是磁盘上的一个文件,也可以是远端的一个服务,或者以其他形式存储的数据。
|
||||
|
||||
这就是通常我们理解的事务。其实这段对事务的描述不是太准确也不完整,但是,它更易于理解,大体上也是正确的。所以我还是倾向于这样来讲“事务”这个比较抽象的概念。
|
||||
|
||||
一个严格意义的事务实现,应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。
|
||||
|
||||
原子性,是指一个事务操作不可分割,要么成功,要么失败,不能有一半成功一半失败的情况。
|
||||
|
||||
一致性,是指这些数据在事务执行完成这个时间点之前,读到的一定是更新前的数据,之后读到的一定是更新后的数据,不应该存在一个时刻,让用户读到更新过程中的数据。
|
||||
|
||||
隔离性,是指一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对正在进行的其他事务是隔离的,并发执行的各个事务之间不能互相干扰,这个有点儿像我们打网游中的副本,我们在副本中打的怪和掉的装备,与其他副本没有任何关联也不会互相影响。
|
||||
|
||||
持久性,是指一个事务一旦完成提交,后续的其他操作和故障都不会对事务的结果产生任何影响。
|
||||
|
||||
大部分传统的单体关系型数据库都完整的实现了ACID,但是,对于分布式系统来说,严格的实现ACID这四个特性几乎是不可能的,或者说实现的代价太大,大到我们无法接受。
|
||||
|
||||
分布式事务就是要在分布式系统中的实现事务。在分布式系统中,在保证可用性和不严重牺牲性能的前提下,光是要实现数据的一致性就已经非常困难了,所以出现了很多“残血版”的一致性,比如顺序一致性、最终一致性等等。
|
||||
|
||||
显然实现严格的分布式事务是更加不可能完成的任务。所以,目前大家所说的分布式事务,更多情况下,是在分布式系统中事务的不完整实现。在不同的应用场景中,有不同的实现,目的都是通过一些妥协来解决实际问题。
|
||||
|
||||
在实际应用中,比较常见的分布式事务实现有2PC(Two-phase Commit,也叫二阶段提交)、TCC(Try-Confirm-Cancel)和事务消息。每一种实现都有其特定的使用场景,也有各自的问题,都不是完美的解决方案。
|
||||
|
||||
事务消息适用的场景主要是那些需要异步更新数据,并且对数据实时性要求不太高的场景。比如我们在开始时提到的那个例子,在创建订单后,如果出现短暂的几秒,购物车里的商品没有被及时清空,也不是完全不可接受的,只要最终购物车的数据和订单数据保持一致就可以了。
|
||||
|
||||
2PC和TCC不是我们本次课程讨论的内容,就不展开讲了,感兴趣的同学可以自行学习。
|
||||
|
||||
## 消息队列是如何实现分布式事务的?
|
||||
|
||||
事务消息需要消息队列提供相应的功能才能实现,Kafka和RocketMQ都提供了事务相关功能。
|
||||
|
||||
回到订单和购物车这个例子,我们一起来看下如何用消息队列来实现分布式事务。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/27/e6/27ebf12e0dc79e00e1e42c8ff0f4e2e6.jpg" alt="">
|
||||
|
||||
首先,订单系统在消息队列上开启一个事务。然后订单系统给消息服务器发送一个“半消息”,这个半消息不是说消息内容不完整,它包含的内容就是完整的消息内容,半消息和普通消息的唯一区别是,在事务提交之前,对于消费者来说,这个消息是不可见的。
|
||||
|
||||
半消息发送成功后,订单系统就可以执行本地事务了,在订单库中创建一条订单记录,并提交订单库的数据库事务。然后根据本地事务的执行结果决定提交或者回滚事务消息。如果订单创建成功,那就提交事务消息,购物车系统就可以消费到这条消息继续后续的流程。如果订单创建失败,那就回滚事务消息,购物车系统就不会收到这条消息。这样就基本实现了“要么都成功,要么都失败”的一致性要求。
|
||||
|
||||
如果你足够细心,可能已经发现了,这个实现过程中,有一个问题是没有解决的。如果在第四步提交事务消息时失败了怎么办?对于这个问题,Kafka和RocketMQ给出了2种不同的解决方案。
|
||||
|
||||
Kafka的解决方案比较简单粗暴,直接抛出异常,让用户自行处理。我们可以在业务代码中反复重试提交,直到提交成功,或者删除之前创建的订单进行补偿。RocketMQ则给出了另外一种解决方案。
|
||||
|
||||
## RocketMQ中的分布式事务实现
|
||||
|
||||
在RocketMQ中的事务实现中,增加了事务反查的机制来解决事务消息提交失败的问题。如果Producer也就是订单系统,在提交或者回滚事务消息时发生网络异常,RocketMQ的Broker没有收到提交或者回滚的请求,Broker会定期去Producer上反查这个事务对应的本地事务的状态,然后根据反查结果决定提交或者回滚这个事务。
|
||||
|
||||
为了支撑这个事务反查机制,我们的业务代码需要实现一个反查本地事务状态的接口,告知RocketMQ本地事务是成功还是失败。
|
||||
|
||||
在我们这个例子中,反查本地事务的逻辑也很简单,我们只要根据消息中的订单ID,在订单库中查询这个订单是否存在即可,如果订单存在则返回成功,否则返回失败。RocketMQ会自动根据事务反查的结果提交或者回滚事务消息。
|
||||
|
||||
这个反查本地事务的实现,并不依赖消息的发送方,也就是订单服务的某个实例节点上的任何数据。这种情况下,即使是发送事务消息的那个订单服务节点宕机了,RocketMQ依然可以通过其他订单服务的节点来执行反查,确保事务的完整性。
|
||||
|
||||
综合上面讲的通用事务消息的实现和RocketMQ的事务反查机制,使用RocketMQ事务消息功能实现分布式事务的流程如下图:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/11/7a/11ea249b164b893fb9c36e86ae32577a.jpg" alt="">
|
||||
|
||||
## 小结
|
||||
|
||||
我们通过一个订单购物车的例子,学习了事务的ACID四个特性,以及如何使用消息队列来实现分布式事务。
|
||||
|
||||
然后我们给出了现有的几种分布式事务的解决方案,包括事务消息,但是这几种方案都不能解决分布式系统中的所有问题,每一种方案都有局限性和特定的适用场景。
|
||||
|
||||
最后,我们一起学习了RocketMQ的事务反查机制,这种机制通过定期反查事务状态,来补偿提交事务消息可能出现的通信失败。在Kafka的事务功能中,并没有类似的反查机制,需要用户自行去解决这个问题。
|
||||
|
||||
但是,这不代表RocketMQ的事务功能比Kafka更好,只能说在我们这个例子的场景下,更适合使用RocketMQ。Kafka对于事务的定义、实现和适用场景,和RocketMQ有比较大的差异,后面的课程中,我们会专门讲到Kafka的事务的实现原理。
|
||||
|
||||
## 思考题
|
||||
|
||||
课后,我建议你最好能通过写代码的方式,把我们这节课中的订单购物车的例子,用RocketMQ完整地实现出来。然后再思考一下,RocketMQ的这种事务消息是不是完整地实现了事务的ACID四个特性?如果不是,哪些特性没有实现?欢迎在留言区与我分享讨论。
|
||||
|
||||
感谢阅读,如果你觉得这篇文章对你有一些启发,也欢迎把它分享给你的朋友。
|
||||
|
||||
|
||||
129
极客时间专栏/geek/消息队列高手课/基础篇/05 | 如何确保消息不会丢失?.md
Normal file
129
极客时间专栏/geek/消息队列高手课/基础篇/05 | 如何确保消息不会丢失?.md
Normal file
@@ -0,0 +1,129 @@
|
||||
<audio id="audio" title="05 | 如何确保消息不会丢失?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/60/5e/6052aad49d746fdf155b535c03d68a5e.mp3"></audio>
|
||||
|
||||
你好,我是李玥。这节课我们来聊聊丢消息的事儿。
|
||||
|
||||
对于刚刚接触消息队列的同学,最常遇到的问题,也是最头痛的问题就是丢消息了。对于大部分业务系统来说,丢消息意味着数据丢失,是完全无法接受的。
|
||||
|
||||
其实,现在主流的消息队列产品都提供了非常完善的消息可靠性保证机制,完全可以做到在消息传递过程中,即使发生网络中断或者硬件故障,也能确保消息的可靠传递,不丢消息。
|
||||
|
||||
绝大部分丢消息的原因都是由于开发者不熟悉消息队列,没有正确使用和配置消息队列导致的。虽然不同的消息队列提供的API不一样,相关的配置项也不同,但是在保证消息可靠传递这块儿,它们的实现原理是一样的。
|
||||
|
||||
这节课我们就来讲一下,消息队列是怎么保证消息可靠传递的,这里面的实现原理是怎么样的。当你熟知原理以后,无论你使用任何一种消息队列,再简单看一下它的API和相关配置项,就能很快知道该如何配置消息队列,写出可靠的代码,避免消息丢失。
|
||||
|
||||
## 检测消息丢失的方法
|
||||
|
||||
我们说,用消息队列最尴尬的情况不是丢消息,而是消息丢了还不知道。一般而言,一个新的系统刚刚上线,各方面都不太稳定,需要一个磨合期,这个时候,特别需要监控到你的系统中是否有消息丢失的情况。
|
||||
|
||||
如果是IT基础设施比较完善的公司,一般都有分布式链路追踪系统,使用类似的追踪系统可以很方便地追踪每一条消息。如果没有这样的追踪系统,这里我提供一个比较简单的方法,来检查是否有消息丢失的情况。
|
||||
|
||||
**我们可以利用消息队列的有序性来验证是否有消息丢失。**原理非常简单,在Producer端,我们给每个发出的消息附加一个连续递增的序号,然后在Consumer端来检查这个序号的连续性。
|
||||
|
||||
如果没有消息丢失,Consumer收到消息的序号必然是连续递增的,或者说收到的消息,其中的序号必然是上一条消息的序号+1。如果检测到序号不连续,那就是丢消息了。还可以通过缺失的序号来确定丢失的是哪条消息,方便进一步排查原因。
|
||||
|
||||
大多数消息队列的客户端都支持拦截器机制,你可以利用这个拦截器机制,在Producer发送消息之前的拦截器中将序号注入到消息中,在Consumer收到消息的拦截器中检测序号的连续性,这样实现的好处是消息检测的代码不会侵入到你的业务代码中,待你的系统稳定后,也方便将这部分检测的逻辑关闭或者删除。
|
||||
|
||||
如果是在一个分布式系统中实现这个检测方法,有几个问题需要你注意。
|
||||
|
||||
首先,像Kafka和RocketMQ这样的消息队列,它是不保证在Topic上的严格顺序的,只能保证分区上的消息是有序的,所以我们在发消息的时候必须要指定分区,并且,在每个分区单独检测消息序号的连续性。
|
||||
|
||||
如果你的系统中Producer是多实例的,由于并不好协调多个Producer之间的发送顺序,所以也需要每个Producer分别生成各自的消息序号,并且需要附加上Producer的标识,在Consumer端按照每个Producer分别来检测序号的连续性。
|
||||
|
||||
Consumer实例的数量最好和分区数量一致,做到Consumer和分区一一对应,这样会比较方便地在Consumer内检测消息序号的连续性。
|
||||
|
||||
## 确保消息可靠传递
|
||||
|
||||
讲完了检测消息丢失的方法,接下来我们一起来看一下,整个消息从生产到消费的过程中,哪些地方可能会导致丢消息,以及应该如何避免消息丢失。
|
||||
|
||||
你可以看下这个图,一条消息从生产到消费完成这个过程,可以划分三个阶段,为了方便描述,我给每个阶段分别起了个名字。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/81/05/81a01f5218614efea2838b0808709205.jpg" alt="">
|
||||
|
||||
- **生产阶段**: 在这个阶段,从消息在Producer创建出来,经过网络传输发送到Broker端。
|
||||
- **存储阶段**: 在这个阶段,消息在Broker端存储,如果是集群,消息会在这个阶段被复制到其他的副本上。
|
||||
- **消费阶段**: 在这个阶段,Consumer从Broker上拉取消息,经过网络传输发送到Consumer上。
|
||||
|
||||
**1. 生产阶段**
|
||||
|
||||
在生产阶段,消息队列通过最常用的请求确认机制,来保证消息的可靠传递:当你的代码调用发消息方法时,消息队列的客户端会把消息发送到Broker,Broker收到消息后,会给客户端返回一个确认响应,表明消息已经收到了。客户端收到响应后,完成了一次正常消息的发送。
|
||||
|
||||
只要Producer收到了Broker的确认响应,就可以保证消息在生产阶段不会丢失。有些消息队列在长时间没收到发送确认响应后,会自动重试,如果重试再失败,就会以返回值或者异常的方式告知用户。
|
||||
|
||||
**你在编写发送消息代码时,需要注意,正确处理返回值或者捕获异常,就可以保证这个阶段的消息不会丢失。**以Kafka为例,我们看一下如何可靠地发送消息:
|
||||
|
||||
同步发送时,只要注意捕获异常即可。
|
||||
|
||||
```
|
||||
try {
|
||||
RecordMetadata metadata = producer.send(record).get();
|
||||
System.out.println("消息发送成功。");
|
||||
} catch (Throwable e) {
|
||||
System.out.println("消息发送失败!");
|
||||
System.out.println(e);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
异步发送时,则需要在回调方法里进行检查。这个地方是需要特别注意的,很多丢消息的原因就是,我们使用了异步发送,却没有在回调中检查发送结果。
|
||||
|
||||
```
|
||||
producer.send(record, (metadata, exception) -> {
|
||||
if (metadata != null) {
|
||||
System.out.println("消息发送成功。");
|
||||
} else {
|
||||
System.out.println("消息发送失败!");
|
||||
System.out.println(exception);
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
**2. 存储阶段**
|
||||
|
||||
在存储阶段正常情况下,只要Broker在正常运行,就不会出现丢失消息的问题,但是如果Broker出现了故障,比如进程死掉了或者服务器宕机了,还是可能会丢失消息的。
|
||||
|
||||
**如果对消息的可靠性要求非常高,可以通过配置Broker参数来避免因为宕机丢消息。**
|
||||
|
||||
对于单个节点的Broker,需要配置Broker参数,在收到消息后,将消息写入磁盘后再给Producer返回确认响应,这样即使发生宕机,由于消息已经被写入磁盘,就不会丢失消息,恢复后还可以继续消费。例如,在RocketMQ中,需要将刷盘方式flushDiskType配置为SYNC_FLUSH同步刷盘。
|
||||
|
||||
如果是Broker是由多个节点组成的集群,需要将Broker集群配置成:至少将消息发送到2个以上的节点,再给客户端回复发送确认响应。这样当某个Broker宕机时,其他的Broker可以替代宕机的Broker,也不会发生消息丢失。后面我会专门安排一节课,来讲解在集群模式下,消息队列是如何通过消息复制来确保消息的可靠性的。
|
||||
|
||||
**3. 消费阶段**
|
||||
|
||||
消费阶段采用和生产阶段类似的确认机制来保证消息的可靠传递,客户端从Broker拉取消息后,执行用户的消费业务逻辑,成功后,才会给Broker发送消费确认响应。如果Broker没有收到消费确认响应,下次拉消息的时候还会返回同一条消息,确保消息不会在网络传输过程中丢失,也不会因为客户端在执行消费逻辑中出错导致丢失。
|
||||
|
||||
你在编写消费代码时需要注意的是,**不要在收到消息后就立即发送消费确认,而是应该在执行完所有消费业务逻辑之后,再发送消费确认。**
|
||||
|
||||
同样,我们以用Python语言消费RabbitMQ消息为例,来看一下如何实现一段可靠的消费代码:
|
||||
|
||||
```
|
||||
def callback(ch, method, properties, body):
|
||||
print(" [x] 收到消息 %r" % body)
|
||||
# 在这儿处理收到的消息
|
||||
database.save(body)
|
||||
print(" [x] 消费完成")
|
||||
# 完成消费业务逻辑后发送消费确认响应
|
||||
ch.basic_ack(delivery_tag = method.delivery_tag)
|
||||
|
||||
channel.basic_consume(queue='hello', on_message_callback=callback)
|
||||
|
||||
```
|
||||
|
||||
你可以看到,在消费的回调方法callback中,正确的顺序是,先是把消息保存到数据库中,然后再发送消费确认响应。这样如果保存消息到数据库失败了,就不会执行消费确认的代码,下次拉到的还是这条消息,直到消费成功。
|
||||
|
||||
## 小结
|
||||
|
||||
这节课我带大家分析了一条消息从发送到消费整个流程中,消息队列是如何确保消息的可靠性,不会丢失的。这个过程可以分为分三个阶段,每个阶段都需要正确的编写代码并且设置正确的配置项,才能配合消息队列的可靠性机制,确保消息不会丢失。
|
||||
|
||||
- 在生产阶段,你需要捕获消息发送的错误,并重发消息。
|
||||
- 在存储阶段,你可以通过配置刷盘和复制相关的参数,让消息写入到多个副本的磁盘上,来确保消息不会因为某个Broker宕机或者磁盘损坏而丢失。
|
||||
- 在消费阶段,你需要在处理完全部消费业务逻辑之后,再发送消费确认。
|
||||
|
||||
你在理解了这几个阶段的原理后,如果再出现丢消息的情况,应该可以通过在代码中加一些日志的方式,很快定位到是哪个阶段出了问题,然后再进一步深入分析,快速找到问题原因。
|
||||
|
||||
## 思考题
|
||||
|
||||
我刚刚讲到,如果消息在网络传输过程中发送错误,由于发送方收不到确认,会通过重发来保证消息不丢失。但是,如果确认响应在网络传输时丢失,也会导致重发消息。也就是说,**无论是Broker还是Consumer都是有可能收到重复消息的,**那我们在编写消费代码时,就需要考虑这种情况,你可以想一下,在消费消息的代码中,该如何处理这种重复消息,才不会影响业务逻辑的正确性?欢迎在留言区与我分享讨论。
|
||||
|
||||
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||
|
||||
|
||||
114
极客时间专栏/geek/消息队列高手课/基础篇/06 | 如何处理消费过程中的重复消息?.md
Normal file
114
极客时间专栏/geek/消息队列高手课/基础篇/06 | 如何处理消费过程中的重复消息?.md
Normal file
@@ -0,0 +1,114 @@
|
||||
<audio id="audio" title="06 | 如何处理消费过程中的重复消息?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/34/00/349f126bd6dc54fb48042e46e073b700.mp3"></audio>
|
||||
|
||||
你好,我是李玥。上节课我们讲了如何确保消息不会丢失,课后我给你留了一个思考题,如果消息重复了怎么办?这节课,我们就来聊一聊如何处理重复消息的问题。
|
||||
|
||||
在消息传递过程中,如果出现传递失败的情况,发送方会执行重试,重试的过程中就有可能会产生重复的消息。对使用消息队列的业务系统来说,如果没有对重复消息进行处理,就有可能会导致系统的数据出现错误。
|
||||
|
||||
比如说,一个消费订单消息,统计下单金额的微服务,如果没有正确处理重复消息,那就会出现重复统计,导致统计结果错误。
|
||||
|
||||
你可能会问,如果消息队列本身能保证消息不重复,那应用程序的实现不就简单了?那有没有消息队列能保证消息不重复呢?
|
||||
|
||||
## 消息重复的情况必然存在
|
||||
|
||||
在MQTT协议中,给出了三种传递消息时能够提供的服务质量标准,这三种服务质量从低到高依次是:
|
||||
|
||||
<li>
|
||||
**At most once**: 至多一次。消息在传递时,最多会被送达一次。换一个说法就是,没什么消息可靠性保证,允许丢消息。一般都是一些对消息可靠性要求不太高的监控场景使用,比如每分钟上报一次机房温度数据,可以接受数据少量丢失。
|
||||
</li>
|
||||
<li>
|
||||
**At least once**: 至少一次。消息在传递时,至少会被送达一次。也就是说,不允许丢消息,但是允许有少量重复消息出现。
|
||||
</li>
|
||||
<li>
|
||||
**Exactly once**:恰好一次。消息在传递时,只会被送达一次,不允许丢失也不允许重复,这个是最高的等级。
|
||||
</li>
|
||||
|
||||
这个服务质量标准不仅适用于MQTT,对所有的消息队列都是适用的。我们现在常用的绝大部分消息队列提供的服务质量都是At least once,包括RocketMQ、RabbitMQ和Kafka 都是这样。也就是说,消息队列很难保证消息不重复。
|
||||
|
||||
说到这儿我知道肯定有的同学会反驳我:“你说的不对,我看过Kafka的文档,Kafka是支持Exactly once的。”我在这里跟这些同学解释一下,你说的没错,Kafka的确是支持Exactly once,但是我讲的也没有问题,为什么呢?
|
||||
|
||||
Kafka支持的“Exactly once”和我们刚刚提到的消息传递的服务质量标准“Exactly once”是不一样的,它是Kafka提供的另外一个特性,Kafka中支持的事务也和我们通常意义理解的事务有一定的差异。在Kafka中,事务和Excactly once主要是为了配合流计算使用的特性,我们在专栏“进阶篇”这个模块中,会有专门的一节课来讲Kafka的事务和它支持的Exactly once特性。
|
||||
|
||||
稍微说一些题外话,Kafka的团队是一个非常善于包装和营销的团队,你看他们很巧妙地用了两个所有人都非常熟悉的概念“事务”和“Exactly once”来包装它的新的特性,实际上它实现的这个事务和Exactly once并不是我们通常理解的那两个特性,但是你深入了解Kafka的事务和Exactly once后,会发现其实它这个特性虽然和我们通常的理解不一样,但确实和事务、Exactly once有一定关系。
|
||||
|
||||
这一点上,我们都要学习Kafka团队。一个优秀的开发团队,不仅要能写代码,更要能写文档,能写Slide(PPT),还要能讲,会分享。对于每个程序员来说,也是一样的。
|
||||
|
||||
我们把话题收回来,继续来说重复消息的问题。既然消息队列无法保证消息不重复,就需要我们的消费代码能够接受“消息是可能会重复的”这一现状,然后,通过一些方法来消除重复消息对业务的影响。
|
||||
|
||||
## 用幂等性解决重复消息问题
|
||||
|
||||
一般解决重复消息的办法是,在消费端,让我们消费消息的操作具备幂等性。
|
||||
|
||||
**幂等(Idempotence)** 本来是一个数学上的概念,它是这样定义的:
|
||||
|
||||
>
|
||||
如果一个函数f(x)满足:f(f(x)) = f(x),则函数f(x)满足幂等性。
|
||||
|
||||
|
||||
这个概念被拓展到计算机领域,被用来描述一个操作、方法或者服务。一个幂等操作的特点是,**其任意多次执行所产生的影响均与一次执行的影响相同。**
|
||||
|
||||
一个幂等的方法,使用同样的参数,对它进行多次调用和一次调用,对系统产生的影响是一样的。所以,对于幂等的方法,不用担心重复执行会对系统造成任何改变。
|
||||
|
||||
我们举个例子来说明一下。在不考虑并发的情况下,“将账户X的余额设置为100元”,执行一次后对系统的影响是,账户X的余额变成了100元。只要提供的参数100元不变,那即使再执行多少次,账户X的余额始终都是100元,不会变化,这个操作就是一个幂等的操作。
|
||||
|
||||
再举一个例子,“将账户X的余额加100元”,这个操作它就不是幂等的,每执行一次,账户余额就会增加100元,执行多次和执行一次对系统的影响(也就是账户的余额)是不一样的。
|
||||
|
||||
如果我们系统消费消息的业务逻辑具备幂等性,那就不用担心消息重复的问题了,因为同一条消息,消费一次和消费多次对系统的影响是完全一样的。也就可以认为,消费多次等于消费一次。
|
||||
|
||||
从对系统的影响结果来说:**At least once + 幂等消费 = Exactly once。**
|
||||
|
||||
那么如何实现幂等操作呢?最好的方式就是,**从业务逻辑设计上入手,将消费的业务逻辑设计成具备幂等性的操作。**但是,不是所有的业务都能设计成天然幂等的,这里就需要一些方法和技巧来实现幂等。
|
||||
|
||||
下面我给你介绍几种常用的设计幂等操作的方法:
|
||||
|
||||
**1. 利用数据库的唯一约束实现幂等**
|
||||
|
||||
例如我们刚刚提到的那个不具备幂等特性的转账的例子:将账户X的余额加100元。在这个例子中,我们可以通过改造业务逻辑,让它具备幂等性。
|
||||
|
||||
首先,我们可以限定,对于每个转账单每个账户只可以执行一次变更操作,在分布式系统中,这个限制实现的方法非常多,最简单的是我们在数据库中建一张转账流水表,这个表有三个字段:转账单ID、账户ID和变更金额,然后给转账单ID和账户ID这两个字段联合起来创建一个唯一约束,这样对于相同的转账单ID和账户ID,表里至多只能存在一条记录。
|
||||
|
||||
这样,我们消费消息的逻辑可以变为:“在转账流水表中增加一条转账记录,然后再根据转账记录,异步操作更新用户余额即可。”在转账流水表增加一条转账记录这个操作中,由于我们在这个表中预先定义了“账户ID转账单ID”的唯一约束,对于同一个转账单同一个账户只能插入一条记录,后续重复的插入操作都会失败,这样就实现了一个幂等的操作。我们只要写一个SQL,正确地实现它就可以了。
|
||||
|
||||
基于这个思路,不光是可以使用关系型数据库,只要是支持类似“INSERT IF NOT EXIST”语义的存储类系统都可以用于实现幂等,比如,你可以用Redis的SETNX命令来替代数据库中的唯一约束,来实现幂等消费。
|
||||
|
||||
**2. 为更新的数据设置前置条件**
|
||||
|
||||
另外一种实现幂等的思路是,给数据变更设置一个前置条件,如果满足条件就更新数据,否则拒绝更新数据,在更新数据的时候,同时变更前置条件中需要判断的数据。这样,重复执行这个操作时,由于第一次更新数据的时候已经变更了前置条件中需要判断的数据,不满足前置条件,则不会重复执行更新数据操作。
|
||||
|
||||
比如,刚刚我们说过,“将账户X的余额增加100元”这个操作并不满足幂等性,我们可以把这个操作加上一个前置条件,变为:“如果账户X当前的余额为500元,将余额加100元”,这个操作就具备了幂等性。对应到消息队列中的使用时,可以在发消息时在消息体中带上当前的余额,在消费的时候进行判断数据库中,当前余额是否与消息中的余额相等,只有相等才执行变更操作。
|
||||
|
||||
但是,如果我们要更新的数据不是数值,或者我们要做一个比较复杂的更新操作怎么办?用什么作为前置判断条件呢?更加通用的方法是,给你的数据增加一个版本号属性,每次更数据前,比较当前数据的版本号是否和消息中的版本号一致,如果不一致就拒绝更新数据,更新数据的同时将版本号+1,一样可以实现幂等更新。
|
||||
|
||||
**3. 记录并检查操作**
|
||||
|
||||
如果上面提到的两种实现幂等方法都不能适用于你的场景,我们还有一种通用性最强,适用范围最广的实现幂等性方法:记录并检查操作,也称为“Token机制或者GUID(全局唯一ID)机制”,实现的思路特别简单:在执行数据更新操作之前,先检查一下是否执行过这个更新操作。
|
||||
|
||||
具体的实现方法是,在发送消息时,给每条消息指定一个全局唯一的ID,消费时,先根据这个ID检查这条消息是否有被消费过,如果没有消费过,才更新数据,然后将消费状态置为已消费。
|
||||
|
||||
原理和实现是不是很简单?其实一点儿都不简单,在分布式系统中,这个方法其实是非常难实现的。首先,给每个消息指定一个全局唯一的ID就是一件不那么简单的事儿,方法有很多,但都不太好同时满足简单、高可用和高性能,或多或少都要有些牺牲。更加麻烦的是,在“检查消费状态,然后更新数据并且设置消费状态”中,三个操作必须作为一组操作保证原子性,才能真正实现幂等,否则就会出现Bug。
|
||||
|
||||
比如说,对于同一条消息:“全局ID为8,操作为:给ID为666账户增加100元”,有可能出现这样的情况:
|
||||
|
||||
<li>
|
||||
t0时刻:Consumer A 收到条消息,检查消息执行状态,发现消息未处理过,开始执行“账户增加100元”;
|
||||
</li>
|
||||
<li>
|
||||
t1时刻:Consumer B 收到条消息,检查消息执行状态,发现消息未处理过,因为这个时刻,Consumer A还未来得及更新消息执行状态。
|
||||
</li>
|
||||
|
||||
这样就会导致账户被错误地增加了两次100元,这是一个在分布式系统中非常容易犯的错误,一定要引以为戒。
|
||||
|
||||
对于这个问题,当然我们可以用事务来实现,也可以用锁来实现,但是在分布式系统中,无论是分布式事务还是分布式锁都是比较难解决问题。
|
||||
|
||||
## 小结
|
||||
|
||||
这节课我们主要介绍了通过幂等消费来解决消息重复的问题,然后我重点讲了几种实现幂等操作的方法,你可以利用数据库的约束来防止重复更新数据,也可以为数据更新设置一次性的前置条件,来防止重复消息,如果这两种方法都不适用于你的场景,还可以用“记录并检查操作”的方式来保证幂等,这种方法适用范围最广,但是实现难度和复杂度也比较高,一般不推荐使用。
|
||||
|
||||
这些实现幂等的方法,不仅可以用于解决重复消息的问题,也同样适用于,在其他场景中来解决重复请求或者重复调用的问题。比如,我们可以将HTTP服务设计成幂等的,解决前端或者APP重复提交表单数据的问题;也可以将一个微服务设计成幂等的,解决RPC框架自动重试导致的重复调用问题。这些方法都是通用的,希望你能做到触类旁通,举一反三。
|
||||
|
||||
## 思考题
|
||||
|
||||
最后请你想一下,为什么大部分消息队列都选择只提供At least once的服务质量,而不是级别更高的Exactly once呢?欢迎在留言区与我分享讨论。
|
||||
|
||||
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||
|
||||
|
||||
87
极客时间专栏/geek/消息队列高手课/基础篇/07 | 消息积压了该如何处理?.md
Normal file
87
极客时间专栏/geek/消息队列高手课/基础篇/07 | 消息积压了该如何处理?.md
Normal file
@@ -0,0 +1,87 @@
|
||||
<audio id="audio" title="07 | 消息积压了该如何处理?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a2/c7/a210a2a59e372748a7e065a573d814c7.mp3"></audio>
|
||||
|
||||
你好,我是李玥。这节课我们来聊一聊关于消息积压的问题。
|
||||
|
||||
据我了解,在使用消息队列遇到的问题中,消息积压这个问题,应该是最常遇到的问题了,并且,这个问题还不太好解决。
|
||||
|
||||
我们都知道,消息积压的直接原因,一定是系统中的某个部分出现了性能问题,来不及处理上游发送的消息,才会导致消息积压。
|
||||
|
||||
所以,我们先来分析下,在使用消息队列时,如何来优化代码的性能,避免出现消息积压。然后再来看看,如果你的线上系统出现了消息积压,该如何进行紧急处理,最大程度地避免消息积压对业务的影响。
|
||||
|
||||
## 优化性能来避免消息积压
|
||||
|
||||
在使用消息队列的系统中,对于性能的优化,主要体现在生产者和消费者这一收一发两部分的业务逻辑中。对于消息队列本身的性能,你作为使用者,不需要太关注。为什么这么说呢?
|
||||
|
||||
主要原因是,对于绝大多数使用消息队列的业务来说,消息队列本身的处理能力要远大于业务系统的处理能力。主流消息队列的单个节点,消息收发的性能可以达到每秒钟处理几万至几十万条消息的水平,还可以通过水平扩展Broker的实例数成倍地提升处理能力。
|
||||
|
||||
而一般的业务系统需要处理的业务逻辑远比消息队列要复杂,单个节点每秒钟可以处理几百到几千次请求,已经可以算是性能非常好的了。所以,对于消息队列的性能优化,我们更关注的是,**在消息的收发两端,我们的业务代码怎么和消息队列配合,达到一个最佳的性能。**
|
||||
|
||||
#### 1. 发送端性能优化
|
||||
|
||||
发送端业务代码的处理性能,实际上和消息队列的关系不大,因为一般发送端都是先执行自己的业务逻辑,最后再发送消息。**如果说,你的代码发送消息的性能上不去,你需要优先检查一下,是不是发消息之前的业务逻辑耗时太多导致的。**
|
||||
|
||||
对于发送消息的业务逻辑,只需要注意设置合适的并发和批量大小,就可以达到很好的发送性能。为什么这么说呢?
|
||||
|
||||
我们之前的课程中讲过Producer发送消息的过程,Producer发消息给Broker,Broker收到消息后返回确认响应,这是一次完整的交互。假设这一次交互的平均时延是1ms,我们把这1ms的时间分解开,它包括了下面这些步骤的耗时:
|
||||
|
||||
- 发送端准备数据、序列化消息、构造请求等逻辑的时间,也就是发送端在发送网络请求之前的耗时;
|
||||
- 发送消息和返回响应在网络传输中的耗时;
|
||||
- Broker处理消息的时延。
|
||||
|
||||
如果是单线程发送,每次只发送1条消息,那么每秒只能发送 1000ms / 1ms * 1条/ms = 1000条 消息,这种情况下并不能发挥出消息队列的全部实力。
|
||||
|
||||
无论是增加每次发送消息的批量大小,还是增加并发,都能成倍地提升发送性能。至于到底是选择批量发送还是增加并发,主要取决于发送端程序的业务性质。简单来说,只要能够满足你的性能要求,怎么实现方便就怎么实现。
|
||||
|
||||
比如说,你的消息发送端是一个微服务,主要接受RPC请求处理在线业务。很自然的,微服务在处理每次请求的时候,就在当前线程直接发送消息就可以了,因为所有RPC框架都是多线程支持多并发的,自然也就实现了并行发送消息。并且在线业务比较在意的是请求响应时延,选择批量发送必然会影响RPC服务的时延。这种情况,比较明智的方式就是通过并发来提升发送性能。
|
||||
|
||||
如果你的系统是一个离线分析系统,离线系统在性能上的需求是什么呢?它不关心时延,更注重整个系统的吞吐量。发送端的数据都是来自于数据库,这种情况就更适合批量发送,你可以批量从数据库读取数据,然后批量来发送消息,同样用少量的并发就可以获得非常高的吞吐量。
|
||||
|
||||
#### 2. 消费端性能优化
|
||||
|
||||
使用消息队列的时候,大部分的性能问题都出现在消费端,如果消费的速度跟不上发送端生产消息的速度,就会造成消息积压。如果这种性能倒挂的问题只是暂时的,那问题不大,只要消费端的性能恢复之后,超过发送端的性能,那积压的消息是可以逐渐被消化掉的。
|
||||
|
||||
要是消费速度一直比生产速度慢,时间长了,整个系统就会出现问题,要么,消息队列的存储被填满无法提供服务,要么消息丢失,这对于整个系统来说都是严重故障。
|
||||
|
||||
所以,我们在设计系统的时候,**一定要保证消费端的消费性能要高于生产端的发送性能,这样的系统才能健康的持续运行。**
|
||||
|
||||
消费端的性能优化除了优化消费业务逻辑以外,也可以通过水平扩容,增加消费端的并发数来提升总体的消费性能。特别需要注意的一点是,**在扩容Consumer的实例数量的同时,必须同步扩容主题中的分区(也叫队列)数量,确保Consumer的实例数和分区数量是相等的。**如果Consumer的实例数量超过分区数量,这样的扩容实际上是没有效果的。原因我们之前讲过,因为对于消费者来说,在每个分区上实际上只能支持单线程消费。
|
||||
|
||||
我见到过很多消费程序,他们是这样来解决消费慢的问题的:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/46/3e/463b28bda587249e74c1f3a5d33a193e.jpg" alt="">
|
||||
|
||||
它收消息处理的业务逻辑可能比较慢,也很难再优化了,为了避免消息积压,在收到消息的OnMessage方法中,不处理任何业务逻辑,把这个消息放到一个内存队列里面就返回了。然后它可以启动很多的业务线程,这些业务线程里面是真正处理消息的业务逻辑,这些线程从内存队列里取消息处理,这样它就解决了单个Consumer不能并行消费的问题。
|
||||
|
||||
这个方法是不是很完美地实现了并发消费?请注意,这是一个非常常见的错误方法! 为什么错误?因为会丢消息。如果收消息的节点发生宕机,在内存队列中还没来及处理的这些消息就会丢失。关于“消息丢失”问题,你可以回顾一下我们的专栏文章《[05 | 如何确保消息不会丢失?](http://time.geekbang.org/column/article/111488)》。
|
||||
|
||||
## 消息积压了该如何处理?
|
||||
|
||||
还有一种消息积压的情况是,日常系统正常运转的时候,没有积压或者只有少量积压很快就消费掉了,但是某一个时刻,突然就开始积压消息并且积压持续上涨。这种情况下需要你在短时间内找到消息积压的原因,迅速解决问题才不至于影响业务。
|
||||
|
||||
导致突然积压的原因肯定是多种多样的,不同的系统、不同的情况有不同的原因,不能一概而论。但是,我们排查消息积压原因,是有一些相对固定而且比较有效的方法的。
|
||||
|
||||
能导致积压突然增加,最粗粒度的原因,只有两种:要么是发送变快了,要么是消费变慢了。
|
||||
|
||||
大部分消息队列都内置了监控的功能,只要通过监控数据,很容易确定是哪种原因。如果是单位时间发送的消息增多,比如说是赶上大促或者抢购,短时间内不太可能优化消费端的代码来提升消费性能,唯一的方法是通过扩容消费端的实例数来提升总体的消费能力。
|
||||
|
||||
如果短时间内没有足够的服务器资源进行扩容,没办法的办法是,将系统降级,通过关闭一些不重要的业务,减少发送方发送的数据量,最低限度让系统还能正常运转,服务一些重要业务。
|
||||
|
||||
还有一种不太常见的情况,你通过监控发现,无论是发送消息的速度还是消费消息的速度和原来都没什么变化,这时候你需要检查一下你的消费端,是不是消费失败导致的一条消息反复消费这种情况比较多,这种情况也会拖慢整个系统的消费速度。
|
||||
|
||||
如果监控到消费变慢了,你需要检查你的消费实例,分析一下是什么原因导致消费变慢。优先检查一下日志是否有大量的消费错误,如果没有错误的话,可以通过打印堆栈信息,看一下你的消费线程是不是卡在什么地方不动了,比如触发了死锁或者卡在等待某些资源上了。
|
||||
|
||||
## 小结
|
||||
|
||||
这节课我们主要讨论了2个问题,一个是如何在消息队列的收发两端优化系统性能,提前预防消息积压。另外一个问题是,当系统发生消息积压了之后,该如何处理。
|
||||
|
||||
优化消息收发性能,预防消息积压的方法有两种,增加批量或者是增加并发,在发送端这两种方法都可以使用,在消费端需要注意的是,增加并发需要同步扩容分区数量,否则是起不到效果的。
|
||||
|
||||
对于系统发生消息积压的情况,需要先解决积压,再分析原因,毕竟保证系统的可用性是首先要解决的问题。快速解决积压的方法就是通过水平扩容增加Consumer的实例数量。
|
||||
|
||||
## 思考题
|
||||
|
||||
课后请你思考一下,在消费端是否可以通过批量消费的方式来提升消费性能?在什么样场景下,适合使用这种方法?或者说,这种方法有什么局限性?欢迎在留言区与我分享讨论。
|
||||
|
||||
感谢阅读,如果你觉得这篇文章对你有一些启发,也欢迎把它分享给你的朋友。
|
||||
|
||||
|
||||
177
极客时间专栏/geek/消息队列高手课/基础篇/08 | 答疑解惑(一) : 网关如何接收服务端的秒杀结果?.md
Normal file
177
极客时间专栏/geek/消息队列高手课/基础篇/08 | 答疑解惑(一) : 网关如何接收服务端的秒杀结果?.md
Normal file
@@ -0,0 +1,177 @@
|
||||
<audio id="audio" title="08 | 答疑解惑(一) : 网关如何接收服务端的秒杀结果?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/85/04/85edf02052ff69704edf510124e91e04.mp3"></audio>
|
||||
|
||||
你好,我是李玥。
|
||||
|
||||
我们的“消息队列高手课”专栏自从上线到现在,同学们的学习热情和参与度都非常高。每一节课都有很多同学留言评论,这些留言里有总结知识分享收获的,有提出精彩问题的,还有给自己加油打气立Flag的,竟然还有说老师长得像黄渤的。我又仔细去看了一下配图,还是真挺像的。下次老师和极客时间的设计师小姐姐说一样,让她们照着吴彦祖来P图。
|
||||
|
||||
同学们每一条的留言我都认真看过,大部分留言我都给出了回复。在基础篇的最后一节课,我来统一解答一下大家都比较关注的一些问题。
|
||||
|
||||
## 1. 网关如何接收服务端的秒杀结果?
|
||||
|
||||
在《[01 | 为什么需要消息队列?](https://time.geekbang.org/column/article/109572)》这节课里面,我们举了一个秒杀的例子,这个例子是用来说明消息队列是如何来实现异步处理的。课后很多同学留言提问,网关在发送消息之后,是如何来接收后端服务的秒杀结果,又如何来给APP返回响应的呢?
|
||||
|
||||
在解答这个问题之前,我需要先说一下,实际生产环境中的秒杀系统,远比我们举的这个例子复杂得多,实现方案也是多种多样的,不是说一定要按照我们这个例子的方式来实现。
|
||||
|
||||
在这个例子中,网关接收后端服务秒杀结果,实现的方式也不只一种,这里我给大家提供一个比较简单的方案。
|
||||
|
||||
比如说,用Java语言来举例子:
|
||||
|
||||
```
|
||||
public class RequestHandler {
|
||||
|
||||
// ID生成器
|
||||
@Inject
|
||||
private IdGenerator idGenerator;
|
||||
// 消息队列生产者
|
||||
@Inject
|
||||
private Producer producer;
|
||||
// 保存秒杀结果的Map
|
||||
@Inject
|
||||
private Map<Long, Result> results;
|
||||
|
||||
// 保存mutex的Map
|
||||
private Map<Long, Object> mutexes = new ConcurrentHashMap<>();
|
||||
// 这个网关实例的ID
|
||||
@Inject
|
||||
private long myId;
|
||||
|
||||
@Inject
|
||||
private long timeout;
|
||||
|
||||
// 在这里处理APP的秒杀请求
|
||||
public Response onRequest(Request request) {
|
||||
// 获取一个进程内唯一的UUID作为请求id
|
||||
Long uuid = idGenerator.next();
|
||||
try {
|
||||
|
||||
Message msg = composeMsg(request, uuid, myId);
|
||||
|
||||
// 生成一个mutex,用于等待和通知
|
||||
Object mutex = new Object();
|
||||
mutexes.put(uuid, mutex)
|
||||
|
||||
// 发消息
|
||||
producer.send(msg);
|
||||
|
||||
// 等待后端处理
|
||||
synchronized(mutex) {
|
||||
mutex.wait(timeout);
|
||||
}
|
||||
|
||||
// 查询秒杀结果
|
||||
Result result = results.remove(uuid);
|
||||
|
||||
// 检查秒杀结果并返回响应
|
||||
if(null != result && result.success()){
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
} catch (Throwable ignored) {}
|
||||
finally {
|
||||
mutexes.remove(uuid);
|
||||
}
|
||||
// 返回秒杀失败
|
||||
return Response.fail();
|
||||
}
|
||||
|
||||
// 在这里处理后端服务返回的秒杀结果
|
||||
public void onResult(Result result) {
|
||||
|
||||
Object mutex = mutexes.get(result.uuid());
|
||||
if(null != mutex) { // 如果查询不到,说明已经超时了,丢弃result即可。
|
||||
// 登记秒杀结果
|
||||
results.put(result.uuid(), result);
|
||||
// 唤醒处理APP请求的线程
|
||||
synchronized(mutex) {
|
||||
mutex.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在这个方案中,网关在收到APP的秒杀请求后,直接给消息队列发消息。至于消息的内容,并不一定是APP请求的Request,只要包含足够的字段就行了,比如用户ID、设备ID、请求时间等等。另外,还需要包含这个请求的ID和网关的ID,这些后面我们会用到。
|
||||
|
||||
如果发送消息失败,可以直接给APP返回秒杀失败结果,成功发送消息之后,线程就阻塞等待秒杀结果。这里面不可能无限等待下去,需要设定一个等待的超时时间。
|
||||
|
||||
等待结束之后,去存放秒杀结果的Map中查询是否有返回的秒杀结果,如果有就构建Response,给APP返回秒杀结果,如果没有,按秒杀失败处理。
|
||||
|
||||
这是处理APP请求的线程,接下来我们来看一下,网关如何来接收从后端秒杀服务返回的秒杀结果。
|
||||
|
||||
我们可以选择用RPC的方式来返回秒杀结果,这里网关节点是RPC服务端,后端服务为客户端。之前网关发出去的消息中包含了网关的ID,后端服务可以通过这个网关ID来找到对应的网关实例,秒杀结果中需要包含请求ID,这个请求ID也是从消息中获取的。
|
||||
|
||||
网关收到后端服务的秒杀结果之后,用请求ID为Key,把这个结果保存到秒杀结果的Map中,然后通知对应的处理APP请求的线程,结束等待。我刚刚说过,处理APP请求的线程,在结束等待之后,会去秒杀的结果Map中查询这个结果,然后再给APP返回响应。
|
||||
|
||||
我把这个处理过程的流程图放在这里,便于你理解:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/31/51/310c2802ba1018c08665da8af4800251.jpg" alt=""><br>
|
||||
这个解决方案还不是一个性能最优的方案,处理APP请求的线程需要同步等待秒杀结果。后面课程中我们会专门来讲,如何使用异步方式来提升程序的性能。
|
||||
|
||||
## 2. 详解RocketMQ和Kafka的消息模型
|
||||
|
||||
我在看《[03 | 消息模型:主题和队列有什么区别?](http://time.geekbang.org/column/article/110459)》这节课的留言时发现,不少同学对RocketMQ和kafka的消息模型理解的还不是很透彻,这两个消息队列产品的消息模型是一样的,我在这里,再把这个模型相关的概念,通过一个例子详细地说一说。
|
||||
|
||||
假设有一个主题MyTopic,我们为主题创建5个队列,分布到2个Broker中。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/dd/2f/dd3f4c5e40f58b62c2d89554b579a72f.jpg" alt="">
|
||||
|
||||
先说消息生产这一端,假设我们有3个生产者实例:Produer0,Produer1和Producer2。
|
||||
|
||||
这3个生产者是如何对应到2个Broker的,又是如何对应到5个队列的呢?这个很简单,**不用对应,随便发**。每个生产者可以在5个队列中轮询发送,也可以随机选一个队列发送,或者只往某个队列发送,这些都可以。比如Producer0要发5条消息,可以都发到队列Q0里面,也可以5个队列每个队列发一条。
|
||||
|
||||
然后说消费端,很多同学没有搞清楚消费组、消费者和队列这几个概念的对应关系。
|
||||
|
||||
每个消费组就是一份订阅,它要消费主题MyTopic下,所有队列的全部消息。注意,队列里的消息并不是消费掉就没有了,这里的“消费”,只是去队列里面读了消息,并没有删除,消费完这条消息还是在队列里面。
|
||||
|
||||
多个消费组在消费同一个主题时,消费组之间是互不影响的。比如我们有2个消费组:G0和G1。G0消费了哪些消息,G1是不知道的,也不用知道。G0消费过的消息,G1还可以消费。即使G0积压了很多消息,对G1来说也没有任何影响。
|
||||
|
||||
然后我们再说消费组的内部,一个消费组中可以包含多个消费者的实例。比如说消费组G1,包含了2个消费者C0和C1,那这2个消费者又是怎么和主题MyTopic的5个队列对应的呢?
|
||||
|
||||
由于消费确认机制的限制,这里面有一个原则是,在同一个消费组里面,每个队列只能被一个消费者实例占用。至于如何分配,这里面有很多策略,我就不展开说了。总之保证每个队列分配一个消费者就行了。比如,我们可以让消费者C0消费Q0,Q1和Q2,C1消费Q3和Q4,如果C0宕机了,会触发重新分配,这时候C1同时消费全部5个队列。
|
||||
|
||||
再强调一下,队列占用只是针对消费组内部来说的,对于其他的消费组来说是没有影响的。比如队列Q2被消费组G1的消费者C1占用了,对于消费组G2来说,是完全没有影响的,G2也可以分配它的消费者来占用和消费队列Q2。
|
||||
|
||||
最后说一下消费位置,每个消费组内部维护自己的一组消费位置,每个队列对应一个消费位置。消费位置在服务端保存,并且,**消费位置和消费者是没有关系的**。每个消费位置一般就是一个整数,记录这个消费组中,这个队列消费到哪个位置了,这个位置之前的消息都成功消费了,之后的消息都没有消费或者正在消费。
|
||||
|
||||
我把咱们这个例子的消费位置整理成下面的表格,便于你理解。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2c/85/2c78c72078002444e758f9dde1386585.jpg" alt="">
|
||||
|
||||
你可以看到,这个表格中并没有消费者这一列,也就是说消费者和消费位置是没有关系的。
|
||||
|
||||
## 3. 如何实现单个队列的并行消费?
|
||||
|
||||
下面说一下《[03 | 消息模型:主题和队列有什么区别?](http://time.geekbang.org/column/article/110459)》这节课的思考题:如果不要求严格顺序,如何实现单个队列的并行消费?关于这个问题,有很多的实现方式,在JMQ(京东自研的消息队列产品)中,它实现的思路是这样的。
|
||||
|
||||
比如说,队列中当前有10条消息,对应的编号是0-9,当前的消费位置是5。同时来了三个消费者来拉消息,把编号为5、6、7的消息分别给三个消费者,每人一条。过了一段时间,三个消费成功的响应都回来了,这时候就可以把消费位置更新为8了,这样就实现并行消费。
|
||||
|
||||
这是理想的情况。还有可能编号为6、7的消息响应回来了,编号5的消息响应一直回不来,怎么办?这个位置5就是一个消息空洞。为了避免位置5把这个队列卡住,可以先把消费位置5这条消息,复制到一个特殊重试队列中,然后依然把消费位置更新为8,继续消费。再有消费者来拉消息的时候,优先把重试队列中的那条消息给消费者就可以了。
|
||||
|
||||
这是并行消费的一种实现方式。需要注意的是,并行消费开销还是很大的,不应该作为一个常规的,提升消费并发的手段,如果消费慢需要增加消费者的并发数,还是需要扩容队列数。
|
||||
|
||||
## 4. 如何保证消息的严格顺序?
|
||||
|
||||
很多同学在留言中问,怎么来保证消息的严格顺序?我们多次提到过,主题层面是无法保证严格顺序的,只有在队列上才能保证消息的严格顺序。
|
||||
|
||||
如果说,你的业务必须要求全局严格顺序,就只能把消息队列数配置成1,生产者和消费者也只能是一个实例,这样才能保证全局严格顺序。
|
||||
|
||||
大部分情况下,我们并不需要全局严格顺序,只要保证局部有序就可以满足要求了。比如,在传递账户流水记录的时候,只要保证每个账户的流水有序就可以了,不同账户之间的流水记录是不需要保证顺序的。
|
||||
|
||||
如果需要保证局部严格顺序,可以这样来实现。在发送端,我们使用账户ID作为Key,采用一致性哈希算法计算出队列编号,指定队列来发送消息。一致性哈希算法可以保证,相同Key的消息总是发送到同一个队列上,这样可以保证相同Key的消息是严格有序的。如果不考虑队列扩容,也可以用队列数量取模的简单方法来计算队列编号。
|
||||
|
||||
## 写在最后
|
||||
|
||||
在留言中,很多同学留言提出来,能不能讲一讲某个消息队列的某个功能具体如何配置。我的建议是,你先不要太关注功能、API和配置这些细节,在学习如何使用消息队列的过程中,要保持一定的高度来学习。
|
||||
|
||||
因为使用消息队列,大部分的难点在宏观架构层面,要解决这些难点,你需要掌握消息队列宏观层面上的实现原理和最佳实践,这样,无论你使用什么消息队列,都可以做到游刃有余。在选定了合适的消息队列产品,准备写代码之前,再去文档中查看这些细节都来得及。
|
||||
|
||||
所以,我们专栏的“基础篇”讲消息队列的使用,更多讲的是一些通用的原理。这节课是我们消息队列高手课“基础篇”的最后一节课,完整基础篇的学习后,意味着你已经是一个使用消息队列的小达人了。
|
||||
|
||||
在“进阶篇”中,我们将把学习重点从“如何使用”转为“如何实现”,在学习消息队列的实现技术时,你反而要专注到每一个技术点上,深入下去,把每个细节都要搞清楚、学透。课程的深度、难度也会逐步加强,当然你获得的经验值也会更多。
|
||||
|
||||
希望大家一如既往坚持学习,多思考,多练习,跟老师一起打怪升级,成为真正的高手。
|
||||
|
||||
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user