mq常见学习问题总结笔记

从消息队列常见面试题入手来解析消息队列

今儿咱们就来盘一盘大方向上的消息队列有哪些核心注意点。
核心点有很多,为了更 贴合实际场景 ,我从常见的面试问题入手:
如何保证消息不丢失?
如果处理重复消息? 如何保证消息的有序性?
如果处理消息堆积?
为什么需要消息队列
从本质上来说是因为互联网的快速发展, 业务不断扩张 ,促使技术架构需要不断的演进。
从以前的单体架构到现在的微服务架构,成百上千的服务之间相互调用和依赖。从互联网初期一个服务器上有 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 索引入手,来讲述算法在工程上基于场景的灵活运用。单单是因为看源码的 时候有感而写之。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值