消息队列总览

3 篇文章 0 订阅
2 篇文章 0 订阅

为什么要用消息队列

  • 解耦:A系统调用B系统、C系统,传统的调用是直接调用,但是当B系统说我不需要你提供数据了,这时候A需要改代码,C系统说我不需要某个字段了,这时候A也要改代码,如果又多了一个D系统,A又要写代码。为了实现解耦,引入消息队列,A将产生的数据丢到消息队列中,哪个系统需要 哪个系统就去取;
  • 异步:A系统调用B系统,B系统由于某个需要调用第三方接口超时,导致A系统响应速度慢,而B系统的好坏又不会影响业务逻辑,所以可以改为A异步调用B,A将消息丢到消息队列中,B系统订阅消息,实现A的快速响应;
  • 削峰:当大量流量请求到系统A时,由于数据库的处理能力有限,造成数据库连接异常。使用消息队列,大量请求先丢到消息队列中,系统A根据自身的处理能力批量拉取数据进行处理。生产中,高峰期短暂的消息积压是允许的。

使用消息队列有什么缺点

系统复杂性增加,可用性降低。

  • 系统复杂性增加:加了消息队列,需要保证消息队列的高可用,需要保证消息不会重复消费,需要保证消息的可靠性。
  • 系统的可用性降低:如果消息队列挂了,那么系统也会受到影响。

RocketMQ

RocketMQ架构图

消息生产者(Producer)

负责生产消息,一般由业务系统负责生产消息。一个消息生产者会把业务应用系统里产生的消息发送到Broker服务器。RocketMQ提供多种发送方式,同步发送、异步发送、顺序发送、单向发送。同步和异步方式均需要Broker返回确认信息,单向发送不需要。

消息消费者(Consumer)

负责消费消息,一般是后台系统负责异步消费。一个消息消费者会从Broker服务器拉取消息,并将其提供给应用程序。从用户应用的角度而言提供了两种消费形式:pull拉取式消费、push推动式消费。

名称服务器(NameServer)

NameServer 中维护着 Producer 集群、Broker 集群、 Consumer 集群的服务状态。通过定时发送心跳数据包进行维护更新各个服务的状态。NameServer充当路由消息的提供者。生产者或消费者能够通过NameServer查找各个Topic相应的Broker IP列表。多个NameServer实例组成集群,但相互独立,没有信息交换。

消息代理(Broker)

负责存储/转发消息(转发分为推和拉两种,拉是指Consumer主动从Message Broker获取消息,推是指Message Broker主动将Consumer感兴趣的消息推送给Consumer)

RocketMQ是怎么保证系统高可用的?

  • NameServer高可用

NameServer 可以部署多个,多个NameServer互相独立,不会交换消息。Producer、Broker、Consumer 启动的时候都需要指定多个 NameServer,各个服务的信息会同时注册到多个 NameServer 上,从而能到达高可用。

  • Broker高可用

多Master部署,防止单点故障;
主从结构,消息冗余,防止消息丢失;

Broker的一个主从组:Master节点1+Slave节点N
Master节点提供读写服务,Slave节点只提供读服务

每个主从组,Master节点 不断发送新的 CommitLog 给 Slave节点。 Slave节点 不断上报本地的 CommitLog 已经同步到的位置给 Master节点。Master节点与Slave节点之间通信方式有同步和异步。

RocketMQ如何保证消息的可靠性

为了降低消息丢失的概率,MQ需要进行超时和重传

(1) MQ-client-sender 发送消息给MQ-server
(2) MQ-server接收到消息后,发送 ACK消息给发送方
(3) MQ-client-sender 接收到 ACK消息后,则 消息已经投递成功

如果上述 2 消息丢失或者超时,MQ-client-sender 内的 timer 会重发消息,直到收到 ACK消息,如果重试N次后还未收到,则回调发送失败。需要注意的是,这个过程中 MQ-server 可能会收到同一条消息的多次重发。

对每条消息,MQ系统内部必须生成一个inner-msg-id,作为去重和幂等的依据,这个内部消息ID的特性是:

  • 全局唯一
  • MQ生成,具备业务无关性,对消息发送方和消息接收方屏蔽

(4) MQ-server 将消息发送给 MQ-client-receiver
(5) MQ-client-receiver 得到消息处理业务逻辑
(6) MQ-client-receiver 回复 ACK消息给 MQ-server
(7) MQ-server收到 ACK消息,将已消费的消息删除

如果上述 6 消息丢失或者超时,MQ-server 内的 timer 会重发消息,直到 MQ-server 收到ACK消息 并且 将已消费的消息删除,这个过程也可能会重发多次,MQ-client-receiver 也可能会收到同一条消息的多次重发。

需要强调的是,MQ-client-receiver 回ACK给 MQ-server,是消息消费业务方的主动调用行为,不能由 MQ-client-sender 自动发起,因为MQ系统不知道消费方什么时候真正消费成功。

为了保证业务幂等性,业务消息体中,必须有一个biz-id,作为去重和幂等的依据,这个业务ID的特性是:

  • 对于同一个业务场景,全局唯一
  • 由业务消息发送方生成,业务相关,对MQ透明
  • 由业务消息消费方负责判重,以保证幂等

最常见的业务ID有:支付ID,订单ID,帖子ID等。

RocketMQ如何解决消息顺序

消息有序:指的是可以按照消息的发送顺序来消费。例如:一笔订单产生了 3 条消息,分别是订单创建、订单付款、订单完成。消费时,要按照顺序依次消费才有意义。
如何实现顺序消息:保证生产者——MQServer——消费者是一对一对一关系。
虽然这样可以实现严格的顺序消息,但是却存在很大的并发性能问题,并且需要处理更多的异常。

所以RocketMQ认为应该从业务层上来保证消息顺序而不仅仅是依赖于消息系统。

不关注乱序的应用实际大量存在
队列无序并不意味着消息无序

RocketMQ如何解决消息重复

造成消息重复的根本原因是:网络不可达。只要通过网络交换数据,就无法避免这个问题。
解决方法:

  • 消费端处理消息的业务逻辑保持幂等性
    即只要保持幂等性,不管来多少条重复消息,最后处理结果都是一样的(消费端实现)
  • 保证每条消息都有唯一编号且保证消息处理成功与去重表的日志同时出现。
    可以由消息系统实现,也可以由业务端实现。由消息系统来实现,无疑会对消息系统的吞吐量和高可用有所影响,所以最好由业务端自己处理消息重复问题,这也是RocketMQ不解决消息重复的问题的原因。

RocketMQ不保证消息不重复,如果你的业务需要保证严格的不重复消息,需要你自己在业务端去重。

RocketMQ实现事务消息

以购物场景为例,张三购买物品,账户扣款 100 元的同时,需要保证在下游的会员服务中给该账户增加 100 积分。而扣款的业务和增加积分的业务是在两个不同的应用,正常处理逻辑一般是先扣除100元,然后网络通知积分服务增加100积分。如下图:
在这里插入图片描述
以上过程会存在3个问题:

  1. 账号服务在扣款的时候宕机了,这时候可能扣款成功,也可能扣款失败;
  2. 由于网络稳定性无法保证,通知扣积分服务可能失败,但是扣款成功了;
  3. 扣款成功,并且通知成功,但是增加积分的时候失败了。

实际上,rocketmq的事务消息解决的是问题1和问题2这种场景,也就是解决本地事务执行与消息发送的原子性问题。即解决Producer执行业务逻辑成功之后投递消息可能失败的场景。

而对于问题3这种场景,rocketmq提供了消费失败重试的机制。

事务消息的实现思路和过程

RocketMQ 事务消息的设计流程同样借鉴了两阶段提交理论,通过在执行本地事务前后发送两条消息来保证本地事务与发送消息的原子性,过程如下图:
在这里插入图片描述

事务消息详细过程说明
  1. 事务发起方首先发送 prepare 消息到 MQ。
  2. 在发送 prepare 消息成功后执行本地事务。
  3. 根据本地事务执行结果返回 commit 或者是 rollback。
  4. Producer发送确认消息到broker(也就是将步骤3执行的结果发送给broker),这里可能broker未收到确认消息,下面分两种情况分析:
    • 如果broker收到了确认消息:如果收到的结果是commit,则broker视为整个事务过程执行成功,将消息下发给Conusmer端消费;如果收到的结果是rollback,则broker视为本地事务执行失败,broker删除Half消息,不下发给consumer。
    • 如果broker未收到了确认消息:broker定时回查本地事务的执行结果;(由用户实现)如果本地事务已经执行则返回commit;如果未执行,则返回rollback;
  5. Consumer 端的消费成功机制有 MQ 保证。

Producer发送消息

Procucer通过轮询某topic下的所有队列的方式来实现发送方的负载均衡,如下图所示:
在这里插入图片描述
在整个应用生命周期中,生产者需要调用一次start方法来初始化,初始化主要完成的任务有:

  1. 如果没有指定NameServer地址,则会自动寻址
  2. 启动定时任务:更新NameServer地址,从NameServer更新Topic路由信息、清理已经挂掉的broker、向所有broker发送心跳……
  3. 启动负载均衡的服务

初始化完成后,生产者则开始发送消息。如果生产者发送消息失败,则会自动重试。

MQ存储消息

RocketMQ的消息存储是由consume queue和commit log配合完成的。

Comsume queue

consume queue是消息的逻辑队列,用来指定消息在物理文件commit log上的物理位置
每个topic下的每个队列都有一个对应的consume queue文件。

Consume Queue中存储单元是一个20字节定长的二进制数据,顺序写顺序读,如下图所示:
comsume queue文件存储单元格式

Commit log

commit log:消息存放的物理文件,每台broker上的commit log被本机所有的queue共享,不做任何区分。

CommitLog的消息存储单元长度不固定,文件顺序写,随机读。消息的存储结构如下表所示,按照编号顺序以及编号对应的内容依次存储。
在这里插入图片描述

RocketMQ消息订阅

RocketMQ消息订阅有两种模式,一种是Push模式,即MQServer主动向消费端推送;另外一种是Pull模式,即消费端在需要时,主动到MQServer拉取。但在具体实现时,Push和Pull模式都是采用消费端主动拉取的方式。

消费端的Push模式是通过长轮询的模式来实现的,就如同下图:
在这里插入图片描述
Consumer端每隔一段时间主动向broker发送拉消息请求,broker在收到pull请求后,如果有消息则立即返回数据,Comsumer端收到返回的消息后,再回调消费者设置的Listener方法。如果broker在收到pull请求时,消息队列里没有数据,broker端会阻塞请求直到有数据传递或超时才返回。

当然,Consumer端是通过一个线程将阻塞队列LinkedBlockingQueue中的PullRequest发送到broker中去拉取消息,以防止Consumer一直被阻塞。而Broker端,在接收到Consumer的PullRequest请求时,如果发现没有消息,则将PullRequest扔到ConcurrentHashMap中缓存起来。Broker在启动时,会启动一个线程不停地从ConcurrentHashMap中取出PullRequest进行检查,直到有数据返回。

消息队列对比

RocketMQ支持顺序消息、消息重试和分布式事务消息,Kafka仅支持顺序消息,但是Kafka的消息堆积能力比RocketMQ强。

RocketMQ和ActiveMQ的区别

RocketMQ模型简单、接口易用,在阿里大规模使用,社区活跃,单机吞吐量10万级,可用性非常高,消息理论上不会丢失;

  • ActiveMQ严格遵循JMS规范,可持久化到内存、文件、数据库,可用性高主要是主从,多语言支持,消失丢失率低;

  • RocketMQ持久化到磁盘文件,可用性非常高,支持分布式,只支持Java,消息理论上不会丢失;

主要参考文章

https://segmentfault.com/a/1190000015301449

https://juejin.im/post/5c8237b5f265da2dce1f71dc

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值