从消息队列常见面试题入手来解析消息队列
今儿咱们就来盘一盘大方向上的消息队列有哪些核心注意点。
核心点有很多,为了更
贴合实际场景
,我从常见的面试问题入手:
如何保证消息不丢失?
如果处理重复消息?
如何保证消息的有序性?
如果处理消息堆积?
为什么需要消息队列
从本质上来说是因为互联网的快速发展,
业务不断扩张
,促使技术架构需要不断的演进。
从以前的单体架构到现在的微服务架构,成百上千的服务之间相互调用和依赖。从互联网初期一个服务器上有 100
个在线用户已经很了不得,到现在坐拥
10
亿日活的微信。我们需要有一个「东西」来解耦 服务之间的关系、控制资源合理合时的使用以及缓冲流量洪峰等等。
消息队列就应运而生了。它常用来实现:
异步处理、服务解耦、流量控制
。
异步处理
随着公司的发展你可能会发现你项目的
请求链路越来越长
,例如刚开始的电商项目,可以就是粗暴的扣库存、下单。慢慢地又加上积分服务、短信服务等。这一路同步调用下来客户可能等急了,这时候就是 消息队列登场的好时机。
调用链路长、响应就慢了
,并且相对于扣库存和下单,积分和短信没必要这么的
"
及时
"
。因此只需要在 下单结束那个流程,扔个消息到消息队列中就可以直接返回响应了。而且积分服务和短信服务可以并行的消费这条消息。
可以看出消息队列可以
减少请求的等待,还能让服务异步并发处理,提升系统总体性能
。
服务解耦
上面我们说到加了积分服务和短信服务,这时候可能又要来个营销服务,之后领导又说想做个大数据,
又来个数据分析服务等等。
可以发现订单的下游系统在不断的扩充,为了迎合这些下游系统订单服务需要经常地修改,任何一个下
游系统接口的变更可能都会影响到订单服务,这订单服务组可疯了,
真
·
「核心」项目组
。
所以一般会选用消息队列来解决系统之间耦合的问题,订单服务把订单相关消息塞到消息队列中,下游
系统谁要谁就订阅这个主题。这样订单服务就解放啦!
流量控制
想必大家都听过「削峰填谷」,后端服务相对而言都是比较「弱」的,因为业务较重,处理时间较长。
像一些例如秒杀活动爆发式流量打过来可能就顶不住了。因此需要引入一个中间件来做缓冲,消息队列再适合不过了。
网关的请求先放入消息队列中,后端服务尽自己最大能力去消息队列中消费请求。超时的请求可以直接返回错误。
当然还有一些服务特别是某些后台任务,不需要及时地响应,并且业务处理复杂且流程长,那么过来的请求先放入消息队列中,后端服务按照自己的节奏处理。这也是很 nice
的。
上面两种情况分别对应着生产者生产过快和消费者消费过慢两种情况,消息队列都能在其中发挥很好的缓冲效果。
消息队列有两种模型:
队列模型
和
发布
/
订阅模型
。
队列模型
生产者往某个队列里面发送消息,一个队列可以存储多个生产者的消息,一个队列也可以有多个消费者, 但是消费者之间是竞争关系,即每条消息只能被一个消费者消费。
发布
/
订阅模型
为了解决一条消息能被多个消费者消费的问题
,发布
/
订阅模型就来了。该模型是将消息发往一个
Topic
即主题中,所有订阅了这个
Topic
的订阅者都能消费这条消息。
其实可以这么理解,发布
/
订阅模型等于我们都加入了一个群聊中,我发一条消息,加入了这个群聊的人都能收到这条消息。
那么队列模型就是一对一聊天,我发给你的消息,只能在你的聊天窗口弹出,是不可能弹出到别人的聊 天窗口中的。
讲到这有人说,那我一对一聊天对每个人都发同样的消息不就也实现了一条消息被多个人消费了嘛。
是的,通过多队列全量存储相同的消息,即数据的冗余可以实现一条消息被多个消费者消费。
RabbitMQ
就是采用队列模型,通过
Exchange
模块来将消息发送至多个队列,解决一条消息需要被多个消费者消费问题。 这里还能看到假设群聊里除我之外只有一个人,那么此时的发布/
订阅模型和队列模型其实就一样了。
小结一下
队列模型每条消息只能被一个消费者消费,而发布
/
订阅模型就是为让一条消息可以被多个消费者消费而生的,当然队列模型也可以通过消息全量存储至多个队列来解决一条消息被多个消费者消费问题,但 是会有数据的冗余。
发布
/
订阅模型兼容队列模型
,即只有一个消费者的情况下和队列模型基本一致。
RabbitMQ
采用队列模型,
RocketMQ
和
Kafka
采用发布
/
订阅模型。
一般我们称发送消息方为生产者
Producer
,接受消费消息方为消费者
Consumer
,消息队列服务端为 Broker 。
消息从
Producer
发往
Broker
,
Broker
将消息存储至本地,然后
Consumer
从
Broker
拉取消息,或者 Broker
推送消息至
Consumer
,最后消费。
为了提高并发度,往往
发布
/
订阅模型
还会引入
队列
或者
分区
的概念。即消息是发往一个主题下的某个队列或者某个分区中。 RocketMQ
中叫队列,
Kafka
叫分区,本质一样。
例如某个主题下有
5
个队列,那么这个主题的并发度就提高为
5
,同时可以有
5
个消费者
并行消费
该主题的消息。一般可以采用轮询或者 key hash
取余等策略来将同一个主题的消息分配到不同的队列中。 与之对应的消费者一般都有组的概念 Consumer Group
,
即消费者都是属于某个消费组的。一条消息会发往多个订阅了这个主题的消费组。
假设现在有两个消费组分别是 Group 1 和 Group 2 ,它们都订阅了 Topic-a 。此时有一条消息发往
Topic
-
a
,那么这两个消费组都能接收到这条消息。
然后这条消息实际是写入
Topic
某个队列中,消费组中的某个消费者对应消费一个队列的消息。
在物理上除了副本拷贝之外,一条消息在
Broker
中只会有一份,每个消费组会有自己的
offset
即消费点位来标识消费到的位置。在消费点位之前的消息表明已经消费过了。当然这个 offset
是队列级别的。每个消费组都会维护订阅的 Topic
下的每个队列的
offset
。
如何保证消息不丢失
就我们市面上常见的消息队列而言,只要
配置得当
,我们的消息就不会丢。
先来看看这个图,
可以看到一共有三个阶段,分别是
生产消息、存储消息和消费消息
。我们从这三个阶段分别入手来看看如何确保消息不会丢失。
生产者发送消息至
Broker
,需要处理
Broker
的响应,不论是同步还是异步发送消息,同步和异步回调都需要做好 try
-
catch
,妥善的处理响应,如果
Broker
返回写入失败等错误消息,需要重试发送。 当多次发送失败需要作报警,日志记录等。 这样就能保证在生产消息阶段消息不会丢失。
存储消息
存储消息阶段需要在
消息刷盘之后
再给生产者响应,假设消息写入缓存中就返回响应,那么机器突然断电这消息就没了,而生产者以为已经发送成功了。
如果
Broker
是集群部署,有多副本机制,即消息不仅仅要写入当前
Broker
,
还需要写入副本机中。那配置成至少写入两台机子后再给生产者响应。这样基本上就能保证存储的可靠了。一台挂了还有一台还 在呢(假如怕两台都挂了..
那就再多些)。
消费消息
这里经常会有同学犯错,有些同学当消费者拿到消息之后直接存入内存队列中就直接返回给
Broker
消费成功,这是不对的。
你需要考虑拿到消息放在内存之后消费者就宕机了怎么办。所以我们应该在
消费者真正执行完业务逻辑
之后,再发送给
Broker
消费成功
,这才是真正的消费了。
所以只要我们在消息业务逻辑处理完成之后再给
Broker
响应,那么消费阶段消息就不会丢失。
可以看出,保证消息的可靠性需要
三方配合
。
生产者
需要处理好
Broker
的响应,出错情况下利用重试、报警等手段。
Broker
需要控制响应的时机,单机情况下是消息刷盘后返回响应,集群多副本情况下,即发送至两个
副本及以上的情况下再返回响应。
消费者
需要在执行完真正的业务逻辑之后再返回响应给
Broker
。
但是要注意
消息可靠性增强了,性能就下降了
,等待消息刷盘、多副本同步后返回都会影响性能。因此还是看业务,例如日志的传输可能丢那么一两条关系不大,因此没必要等消息刷盘再响应。
如果处理重复消息
我们先来看看能不能避免消息的重复。
假设我们发送消息,就管发,不管
Broker
的响应,那么我们发往
Broker
是不会重复的。
但是一般情况我们是不允许这样的,这样消息就完全不可靠了,我们的基本需求是消息至少得发到
Broker
上,那就得等
Broker
的响应,那么就可能存在
Broker
已经写入了,当时响应由于网络原因生产者没有收到,然后生产者又重发了一次,此时消息就重复了。
再看消费者消费的时候,假设我们消费者拿到消息消费了,业务逻辑已经走完了,事务提交了,此时需要更新 Consumer offset
了,然后这个消费者挂了,另一个消费者顶上,此时
Consumer offset
还没更新,于是又拿到刚才那条消息,业务又被执行了一遍。于是消息又重复了。
可以看到正常业务而言
消息重复是不可避免的
,因此我们只能从
另一个角度
来解决重复消息的问题。 关键点就是幂等
。既然我们不能防止重复消息的产生,那么我们只能在业务上处理重复消息所带来的影响。
幂等处理重复消息
幂等是数学上的概念,我们就理解为同样的参数多次调用同一个接口和调用一次产生的结果是一致的。 例如这条 SQL
update t1 set money = 150 where id = 1 and money = 100
;
执行多少遍
money
都是
150
,这就
叫幂等。 因此需要改造业务处理逻辑,使得在重复消息的情况下也不会影响最终的结果。
可以通过上面我那条
SQL
一样,做了个
前置条件判断
,即
money = 100
情况,并且直接修改,更通用 的是做个 version
即版本号控制,对比消息中的版本号和数据库中的版本号。
或者通过
数据库的约束例如唯一键
,例如
insert into update on duplicate key...
。
或者
记录关键的
key
,比如处理订单这种,记录订单
ID
,假如有重复的消息过来,先判断下这个
ID
是否已经被处理过了,如果没处理再进行下一步。当然也可以用全局唯一ID
等等。
基本上就这么几个套路,
真正应用到实际中还是得看具体业务细节
。
如何保证消息的有序性
有序性分:
全局有序和部分有序
。
全局有序
如果要保证消息的全局有序,首先只能由一个生产者往
Topic
发送消息,并且一个
Topic
内部只能有 一个队列(分区)。消费者也必须是单线程消费这个队列。这样的消息就是全局有序的!
不过一般情况下我们都不需要全局有序,即使是同步
MySQL Binlog
也只需要保证单表消息有序即可。
部分有序
因此绝大部分的有序需求是部分有序,部分有序我们就可以将
Topic
内部划分成我们需要的队列数,把消息通过特定的策略发往固定的队列中,然后每个队列对应一个单线程处理的消费者。这样即完成了部分有序的需求,又可以通过队列数量的并发来提高消息处理效率。
图中我画了多个生产者,一个生产者也可以,只要同类消息发往指定的队列即可。
如果处理消息堆积
消息的堆积往往是因为
生产者的生产速度与消费者的消费速度不匹配
。有可能是因为消息消费失败反复重试造成的,也有可能就是消费者消费能力弱,渐渐地消息就积压了。
因此我们需要
先定位消费慢的原因
,如果是
bug
则处理
bug
,如果是因为本身消费能力较弱,我们可以优化下消费逻辑,比如之前是一条一条消息消费处理的,这次我们批量处理,比如数据库的插入,一条一条插和批量插效率是不一样的。
假如逻辑我们已经都优化了,但还是慢,那就得考虑水平扩容了,增加
Topic
的队列数和消费者数量,
注意队列数一定要增加
,不然新增加的消费者是没东西消费的。
一个
Topic
中,一个队列只会分配给一
个消费者
。
当然你消费者内部是单线程还是多线程消费那看具体场景。不过要注意上面提高的消息丢失的问题,如果你是将接受到的消息写入内存队列
之后,然后就返回响应给
Broker
,然后多线程向内存队列消费消息,假设此时消费者宕机了,内存队列里面还未消费的消息也就丢了。
如何写个消息中间件
接下来咱们再看看如何写个消息中间件。
首先我们需要明确地提出消息中间件的几个重要角色,分别是生产者、消费者、
Broker
、注册中心。 简述下消息中间件数据流转过程,无非就是生产者生成消息,发送至 Broker
,
Broker
可以暂缓消息, 然后消费者再从 Broker
获取消息,用于消费。 而注册中心用于服务的发现包括:Broker
的发现、生产者的发现、消费者的发现,当然还包括下线, 可以说服务的高可用离不开注册中心。 然后开始简述实现要点,可以同通信讲起:各模块的通信可以基于 Netty
然后自定义协议来实现,注册中心可以利用 zookeeper
、
consul
、
eureka
、
nacos
等等,也可以像
RocketMQ
自己实现简单的 namesrv (这一句话就都是关键词)。
为了考虑扩容和整体的性能,采用分布式的思想,像
Kafka
一样采取分区理念,一个
Topic
分为多个 partition,并且为保证数据可靠性,采取多副本存储,即
Leader
和
follower
,根据性能和数据可靠的权衡提供异步和同步的刷盘存储。 并且利用选举算法保证 Leader
挂了之后
follower
可以顶上,保证消息队列的高可用。 也同样为了提高消息队列的可靠性利用本地文件系统来存储消息,并且采用顺序写的方式来提高性能。 可根据消息队列的特性利用内存映射、零拷贝进一步的提升性能,还可利用像 Kafka 这种批处理思想提 高整体的吞吐。
消息队列设计成推消息还是拉消息?
RocketMQ
和
Kafka
是怎么做的?
推拉模式
首先明确一下推拉模式到底是在讨论消息队列的哪一个步骤,一般而言我们在谈论
推拉模式的时候指的
是
Comsumer
和
Broker
之间的交互
。
默认的认为
Producer
与
Broker
之间就是推的方式,即
Producer
将消息推送给
Broker
,而不是
Broker
主动去拉取消息。 想象一下,如果需要 Broker
去拉取消息,那么
Producer
就必须在本地通过日志的形式保存消息来等
待
Broker
的拉取,如果有很多生产者的话,那么消息的可靠性不仅仅靠
Broker
自身,还需要靠成百上千的 Producer
。
Broker
还能靠多副本等机制来保证消息的存储可靠,而成百上千的
Producer
可靠性就有点难办了,所以默认的 Producer
都是推消息给
Broker
。
所以说有些情况分布式好,而有些时候还是集中管理好。
推模式
推模式指的是消息从
Broker
推向
Consumer
,即
Consumer
被动的接收消息,由
Broker
来主导消息的发送。
我们来想一下推模式有什么好处?
消息实时性高
,
Broker
接受完消息之后可以立马推送给
Consumer
。
对于消费者使用来说更简单
,简单啊就等着,反正有消息来了就会推过来。
推模式有什么缺点?
推送速率难以适应消费速率
,推模式的目标就是以最快的速度推送消息,当生产者往
Broker
发送消息的速率大于消费者消费消息的速率时,随着时间的增长消费者那边可能就“
爆仓
”
了,因为根本消费不过来啊。当推送速率过快就像 DDos
攻击一样消费者就傻了。
并且不同的消费者的消费速率还不一样,身为
Broker
很难平衡每个消费者的推送速率,如果要实现自
适应的推送速率那就需要在推送的时候消费者告诉
Broker
,我不行了你推慢点吧,然后
Broker
需要维护每个消费者的状态进行推送速率的变更。
这其实就增加了
Broker
自身的复杂度。
所以说推模式难以根据消费者的状态控制推送速率,适用于消息量不大、消费能力强要求实时性高的情 况下。
拉模式
拉模式指的是
Consumer
主动向
Broker
请求拉取消息,即
Broker
被动的发送消息给
Consumer
。
我们来想一下拉模式有什么好处?
拉模式主动权就在消费者身上了,
消费者可以根据自身的情况来发起拉取消息的请求
。假设当前消费者 觉得自己消费不过来了,它可以根据一定的策略停止拉取,或者间隔拉取都行。
拉模式下
Broker
就相对轻松了
,它只管存生产者发来的消息,至于消费的时候自然由消费者主动发 起,来一个请求就给它消息呗,从哪开始拿消息,拿多少消费者都告诉它,它就是一个没有感情的工具 人,消费者要是没来取也不关它的事。
拉模式可以更合适的进行消息的批量发送
,基于推模式可以来一个消息就推送,也可以缓存一些消息之后再推送,但是推送的时候其实不知道消费者到底能不能一次性处理这么多消息。而拉模式就更加合理,它可以参考消费者请求的信息来决定缓存多少消息之后批量发送。
拉模式有什么缺点?
消息延迟
,毕竟是消费者去拉取消息,但是消费者怎么知道消息到了呢?所以它只能不断地拉取,但是又不能很频繁地请求,太频繁了就变成消费者在攻击 Broker
了。因此需要降低请求的频率,比如隔个 2 秒请求一次,你看着消息就很有可能延迟
2
秒了。
消息忙请求
,忙请求就是比如消息隔了几个小时才有,那么在几个小时之内消费者的请求都是无效的,在做无用功。
RocketMQ
和
Kafka
都选择了拉模式,当然业界也有基于推模式的消息队列如
ActiveMQ
。
我个人觉得拉模式更加的合适,因为现在的消息队列都有持久化消息的需求,也就是说本身它就有个存储功能,它的使命就是接受消息,保存好消息使得消费者可以消费消息即可。
而消费者各种各样,身为
Broker
不应该有依赖于消费者的倾向,我已经为你保存好消息了,你要就来拿好了。
虽说一般而言
Broker
不会成为瓶颈,因为消费端有业务消耗比较慢,但是
Broker
毕竟是一个中心点, 能轻量就尽量轻量。
那么竟然
RocketMQ
和
Kafka
都选择了拉模式,它们就不怕拉模式的缺点么? 怕,所以它们操作了一 波,减轻了拉模式的缺点。
长轮询
RocketMQ
和
Kafka
都是利用
“
长轮询
”
来实现拉模式,我们就来看看它们是如何操作的。
为了简单化,下面我把消息不满足本次拉取的条数啊、总大小啊等等都统一描述成还没有消息,反正都 是不满足条件。
RocketMQ
中的长轮询
RocketMQ
中的
PushConsumer
其实是披着拉模式的方法,
只是看起来像推模式而已
。
因为
RocketMQ
在被背后偷偷的帮我们去
Broker
请求数据了。 后台会有个 RebalanceService
线程,这个线程会根据
topic
的队列数量和当前消费组的消费者个数做 负载均衡,每个队列产生的 pullRequest
放入阻塞队列
pullRequestQueue
中。然后又有个 PullMessageService 线程不断的从阻塞队列
pullRequestQueue
中获取
pullRequest
,然后通过网络请求 broker
,这样实现的准实时拉取消息。 这一部分代码我不截了,就是这么个事儿,稍后会用图来展示。
然后
Broker
的
PullMessageProcessor
里面的
processRequest
方法是用来处理拉消息请求的,有消息就直接返回,如果没有消息怎么办呢?我们来看一下代码。
而
PullRequestHoldService
这个线程会每
5
秒从
pullRequestTable
取
PullRequest
请求,然后看看待拉取消息请求的偏移量是否小于当前消费队列最大偏移量,如果条件成立则说明有新消息了,则会调用 notifyMessageArriving ,最终调用
PullMessageProcessor
的
executeRequestWhenWakeup()
方法重新尝试处理这个消息的请求,也就是再来一次,整个长轮询的时间默认 30
秒。
简单的说就是
5
秒会检查一次消息时候到了,如果到了则调用
processRequest
再处理一次。这好像不 太实时啊? 5
秒?
别急,还有个
ReputMessageService
线程,这个线程用来不断地从
commitLog
中解析数据并分发请
求,构建出
ConsumeQueue
和
IndexFile
两种类型的数据,
并且也会有唤醒请求的操作,来弥补每
5s
一次这么慢的延迟
代码我就不截了,就是消息写入并且会调用
pullRequestHoldService#notifyMessageArriving
。
最后我再来画个图,描述一下整个流程。
小结一下
可以看到
RocketMQ
和
Kafka
都是采用
“
长轮询
”
的机制,具体的做法都是通过消费者等待消息,当有消息的时候 Broker
会直接返回消息,如果没有消息都会采取延迟处理的策略,并且为了保证消息的及时性,在对应队列或者分区有新消息到来的时候都会提醒消息来了,及时返回消息。
一句话说就是消费者和
Broker
相互配合,拉取消息请求不满足条件的时候
hold
住,避免了多次频繁的拉取动作,当消息一到就提醒返回。
最后
总的而言推拉模式各有优劣,而我个人觉得一般情况下拉模式更适合于消息队列。
消息队列之事务消息?
RocketMQ
和
Kafka
是
怎么做的?
今天我们来谈一谈消息队列的事务消息,一说起事务相信大家都不陌生,脑海里蹦出来的就是
ACID
。
通常我们理解的事务就是为了一些更新操作要么都成功,要么都失败,不会有中间状态的产生,而
ACID
是一个严格的事务实现的定义,不过在单体系统时候一般都不会严格的遵循
ACID
的约束来实现事务,更别说分布式系统了。
分布式系统往往只能妥协到最终一致性
,保证数据最终的完整性和一致性,主要原因就是实力不允许
...
因为可用性为王。
而且要保证完全版的事务实现代价很大,你想想要维护这么多系统的数据,不允许有中间状态数据可以被读取,所有的操作必须不可分割,这意味着一个事务的执行是阻塞的,资源是被长时间锁定的。
在高并发情况下资源被长时间的占用,就是致命的伤害,举一个有味道的例子,如厕高峰期,好了懂得都懂。对了,
ACID
是什么还不太清楚的同学,赶紧去查一查,这里我就不展开说了。
分布式事务
那说到分布式事务,常见的有
2PC
、
TCC
和事务消息,这篇文章重点就是事务消息,不过
2PC
和
TCC
我稍微提一下。
2PC
2PC
就是二阶段提交,分别有协调者和参与者两个角色,二阶段分别是准备阶段和提交阶段。
准备阶段就是协调者向各参与者发送准备命令,这个阶段参与者除了事务的提交啥都做了,而提交阶段就是协调者看看各个参与者准备阶段都 o
不
ok
,如果有
ok
那么就向各个参与者发送提交命令,如果有 一个不 ok
那么就发送回滚命令。 这里的重点就是 2PC
只适用于数据库层面的事务
,什么意思呢?就是你想在数据库里面写一条数据同时又要上传一张图片,这两个操作 2PC
无法保证两个操作满足事务的约束。
而且
2PC
是一种
强一致性
的分布式事务,它是
同步阻塞
的,即在接收到提交或回滚命令之前,所有参与者都是互相等待,特别是执行完准备阶段的时候,此时的资源都是锁定的状态,假如有一个参与者卡了很久,其他参与者都得等它,产生长时间资源锁定状态下的阻塞
。
总体而言效率低
,并且存在
单点故障
问题,协调者是就是那个单点,并且在极端条件下存在
数据不一致
的风险,例如某个参与者未收到提交命令,此时宕机了,恢复之后数据是回滚的,而其他参与者其实都已经执行了提交事务的命令了。
TCC
TCC
能保证业务层面的事务
,也就是说它不仅仅是数据库层面,上面的上传图片这种操作它也能做。 TCC 分为三个阶段
try - confifirm - cancel
,简单的说就是每个业务都需要有这三个方法,先都执行
try 方法,这一阶段不会做真正的业务操作,只是先占个坑,什么意思呢?比如打算加 10
个积分,那先在预添加字段加上这 10
积分,这个时候用户账上的积分其实是没有增加的。
然后如果都
try
成功了那么就执行
confifirm
方法,大家都来做真正的业务操作,如果有一个
try
失败了那么大家都执行 cancel
操作,来撤回刚才的修改。
可以看到
TCC
其实对业务的耦合性很大
,因为业务上需要做一定的改造才能完成这三个方法,这其实就是 TCC
的缺点,
并且
confifirm
和
cancel
操作要注意幂等
,因为到执行这两步的时候没有退路,是务必要完成的,因此需要有重试机制,所以需要保证方法幂等。
事务消息
事务消息就是今天文章的主角了,它
主要是适用于异步更新的场景,并且对数据实时性要求不高的地
方
。
它的目的是为了
解决消息生产者与消息消费者的数据一致性问题。
比如你点外卖,我们先选了炸鸡加入购物车,又选了瓶可乐,然后下单,付完款这个流程就结束了。
而购物车里面的数据就很适合用消息通知异步删除,因为一般而言我们下完单不会再去点开这个店家的菜单,而且就算点开了购物车里还有这些菜品也没有关系,影响不大。
我们希望的就是下单成功之后购物车的菜品最终会被删除,所以要点就是
下单和发消息这两个步骤要么
都成功要么都失败
。
RocketMQ
事务消息
我们先来看一下
RocketMQ
是如何实现事务消息的。
RocketMQ
的事务消息也可以被认为是一个两阶段提交,简单的说就是在事务开始的时候会先发送一个半消息给 Broker
。
半消息的意思就是这个消息此时对
Consumer
是不可见的,而且也不是存在真正要发送的队列中,而是一个特殊队列。
发送完半消息之后再执行本地事务,再根据本地事务的执行结果来决定是向
Broker
发送提交消息,还是发送回滚消息。
此时有人说这一步发送提交或者回滚消息失败了怎么办?
影响不大,
Broker
会定时的向
Producer
来反查这个事务是否成功,具体的就是
Producer
需要暴露一 个接口,通过这个接口 Broker
可以得知事务到底有没有执行成功,没成功就返回未知,因为有可能事务还在执行,会进行多次查询。
如果成功那么就将半消息恢复到正常要发送的队列中,这样消费者就可以消费这条消息了。
Kafka
事务消息
Kafka
的事务消息和
RocketMQ
的事务消息又不一样了,
RocketMQ
解决的是本地事务的执行和发消息这两个动作满足事务的约束。
而
Kafka
事务消息则是用在一次事务中需要发送多个消息的情况,保证多个消息之间的事务约束,即多条消息要么都发送成功,要么都发送失败,就像下面代码所演示的。
Kafka
的事务基本上是配合其幂等机制来实现
Exactly Once
语义的
,所以说
Kafka
的事务消息不是我们想的那种事务消息,RocketMQ
的才是。
讲到这我就想扯一下了,说到这个
Exactly Once
其实不太清楚的同学很容易会误解。
我们知道消息可靠性有三种,分别是最多一次、恰好一次、最少一次,之前在消息队列连环问的文章我已经提到了基本上我们都是用最少一次然后配合消费者端的幂等来实现恰好一次。
消息恰好被消费一次当然我们所有人追求的,但是之前文章我已经从各方面已经分析过了,基本上难以达到。
而
Kafka
竟说它能实现
Exactly Once
?这么牛啤吗?这其实是
Kafka
的一个噱头,你要说他错,他还真没错,你要说他对但是他实现的 Exactly Once
不是你心中想的那个
Exactly Once
。
它的恰好一次只能存在一种场景,就是从
Kafka
作为消息源,然后做了一番操作之后,再写入
Kafka
中
。
那他是如何实现恰好一次的?就是通过幂等,和我们在业务上实现的一样通过一个唯一
Id
, 然后记录 下来,如果已经记录过了就不写入,这样来保证恰好一次。
所以说
Kafka
实现的是在特定场景下的恰好一次,不是我们所想的利用
Kafka
来发送消息,那么这条
消息只会恰巧被消费一次
。
这其实和
Redis
说他实现事务了一样,也不是我们心想的事务。
所以开源软件说啥啥特性开发出来了,我们一味的相信,因此其往往都是残血的或者在特殊的场景下才能满足,不要被误导了,不能相信表面上的描述,还得详细的看看文档或者源码。
不过从另一个角度看也无可厚非,作为一个开源软件肯定是想更多的人用,我也没说谎呀,我文档上写的很清楚的,这标题也没骗人吧?
确实,比如你点进震惊
xxxx
标题的文章,人家也没骗你啥,他自己确实震惊的呢。
再回来谈
Kafka
的事务消息,所以说这个事务消息不是我们想要的那个事务消息,其实不是今天的主题了,不过我还是简单的说一下。
Kafka
的事务有事务协调者角色,事务协调者其实就是
Broker
的一部分。
在开始事务的时候,生产者会向事务协调者发起请求表示事务开启,事务协调者会将这个消息记录到特
殊的日志
-
事务日志中,然后生产者再发送真正想要发送的消息,这里
Kafka
和
RocketMQ
处理不一 样,Kafka
会像对待正常消息一样处理这些事务消息,
由消费端来过滤这个消息
。
然后发送完毕之后生产者会向事务协调者发送提交或者回滚请求,由事务协调者来进行两阶段提交,如
果是提交那么会先执行预提交,即把事务的状态置为预提交然后写入事务日志,然后再向所有事务有关的分区写入一条类似事务结束的消息,这样消费端消费到这个消息的时候就知道事务好了,可以把消息放出来了。
最后协调者会向事务日志中再记一条事务结束信息,至此
Kafka
事务就完成了,我拿
conflfluent.io
上的 图来总结一下这个流程。
比
RocketMQ
更好的事务消息实现是什么?
先抛出的一个问题:一个事务涉及
mysql
和
mq
,到底哪个写入成功重要?
假如线上
mq
集群网络故障,导致发消息失败,即使
mysql
还是活着的,但是无法进行事务。
所以其实这个问题问的是:
mysql
和
mq
之间的写入顺序。
在
RocketMQ
中,事务消息的实现方案是先发半消息(半消息对消费者不可见),待半消息发送成功 之后,才能执行本地事务,等本地事务执行成功之后,再向 Broker
发送请求将半消息转成正常消息, 这样消费者就可以消费此消息。
这种顺序等于先得成功写入
mq
,然后再写入数据库,这样的模式会出现一个问题:
即
mq
集群挂了,
事务就无法继续进行了,等于整个应用无法正常执行了
。
看一下我之前画的
RocketMQ
事务消息流程图:
第一步是需要等待半消息的响应,如果响应失败就无法执行本地事务。
看下伪代码,可能更清晰:
result
=
sendHalfMsg
();
//
发送半消息
if
(
result
.
success
) {
执行本地事务
}
else
{
回滚此次事务
}
Kafka
系列
Kafka
的索引设计有什么亮点?
其实这篇文章只是从
Kafka
索引入手,来讲述算法在工程上基于场景的灵活运用。单单是因为看源码的 时候有感而写之。