聊聊RocketMQ

一、RocketMQ的架构是怎么样的?

RocketMQ主要由Producer、Broker和Consumer三部分组成,如下图所示:

  1. Producer:消息生产者,负责将消息发送到Broker。
  2. Broker:消息中转服务器,负责存储和转发消息。RocketMQ支持多个Broker构成集群,每个Broker都拥有独立的存储空间和消息队列。
  3. Consumer:消息消费者,负责从Broker消费消息。
  4. NameServer:名称服务,负责维护Broker的元数据信息,包括Broker地址、Topic和Queue等信息。
  5. Producer和Consumer在启动时需要连接到NameServer获取Broker的地址信息。
  6. Topic:消息主题,是消息的逻辑分类单位。Producer将消息发送到特定的Topic中,Consumer从指定的Topic中消费消息。
  7. Message Queue:消息队列,是Topic的物理实现。一个Topic可以有多个Queue,每个Queue都是独立的存储单元。Producer发送的消息会被存储到对应的Queue中,Consumer从指定的Queue中消费消息

二、RocketMQ的事务消息是如何实现的?

RocketMQ的事务消息是通过TransactionListener接囗来实现的,

在发送事务消息时,首先向RocketMQ Broker发送一条 “half消息”(即半消息),半消息将被存储在Broker端的事务消息日志中,但是这个消息还不能被消费者消费。

接下来,在半消息发送成功后,应用程序通过执行本地事务来确定是否要提交该事务消息。

如果本地事务执行成功,就会通知RocketMQ Broker提交该事务消息,使得该消息可以被消费者消费;否则,就会通知RocketMQ Broker回滚该事务消息,该消息将被删除,从而保证消息不会被消费者消费,

拆解下来的话,主要有以下4个步骤:

  1. 发送半消息:应用程序向RocketMQ Broker发送一条半消息,该消息在Broker端的事务消息日志中被标记为"prepared”状态。
  2. 执行本地事务:RocketMQ会通知应用程序执行本地事务。如果本地事务执行成功,应用程序通知RocketMQ Broker提交该事务消息。
  3. 提交事务消息:RocketMQ收到提交消息以后,会将该消息的状态从“prepared”改为“committed”,并使该消息可以被消费者消费。
  4. 回滚事务消息:如果本地事务执行失败,应用程序通知RocketMQ Broker回滚该事务消息,RocketMO将该消息的状态从“prepared”改为“rolback”,并将该消息从事务消息日志中删除,从而保证该消息不会被消费者消费。

1、如果一直没收到COMMIT或者ROLLBACK怎么办?

在RocketMQ的事务消息中,如果半消息发送成功后,RocketMQ Broker在规定时间内没有收到COMMIT或者ROLLBACK消息。

RocketMO会向应用程序发送一条检查请求,应用程序可以通过回调方法返回是否要提交或回滚该事务消息。如果应用程序在规定时间内未能返回响应,RocketMQ会将该消息标记为“UNKNOW”状态。

在标记为“UNKNOW”状态的事务消息中,如果应用程序有了明确的结果,还可以向MO发送COMMIT或者ROLLBACK.

但是MO不会一直等下去,如果过期时间已到,RocketMO会自动回滚该事务消息,将其从事务消息日志中删除。

2、第一次发送半消息失败了怎么办?

在事务消息的一致性方案中,我们是先发半消息,再做业务操作的

所以,如果半消息发失败了,那么业务操作也不会进行,不会有不一致的问题。

遇到这种情况重试就行了。(可以自己重试,也可以依赖上游重试)

3、为什么要用事务消息?

很多人看完事务消息会有一个疑惑:本地事务执行完成之后再发送消息有什么区别?为什么要有事务消息呢?

主要是因为:本地事务执行完成之后再发送消息可能会发消息失败。

一旦发送消息失败了,那么本地事务提交了,但是消息没成功,那么监听者就收不到消息,那么就产生数据不一致

那如果用事务消息。先提交一个半消息,然后执行本地事务,再发送一个commit的半消息。如果后面这个commit半消息失败了,MQ是可以基于第一个半消息不断反查来推进状态的。这样只要本地事务提交成功,最终MQ也会成功。如果本地事务rolback,那么MQ的消息也会rollback。保证了一致性。

三、RocketMQ如何保证消息的顺序性?

和Kafka只支持同一个Partition内消息的顺序性一样,RocketMQ中也提供了基于队列(分区)的顺序消费。即同个队列内的消息可以做到有序,但是不同队列内的消息是无序的!

当我们作为MQ的生产者需要发送顺序消息时,需要在send方法中,传入一个MessageQueueSelector。

MessageQueueSelector中需要实现一个select方法,这个方法就是用来定义要把消息发送到哪个MessageQueue的,通常可以使用取模法进行路由:

    SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
        @Override
        //mgs:该Topic下所有可选的MessageQueue
        待发送的消息//msg:

        //arg:发送消息时传递的参数
        public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
            Integer id = (Integer) arg;
          //根据参数,计算出一个要接收消息的MessageQueue的下标
            int index = id % mgs.size();
        //返回这个MessageQueue
            return mgs.get(index);
        }
    },orderId);

通过以上形式就可以将需要有序的消息发送到同一个队列中。需要注意的时候,这里需要使用同步发送的方式!

消息按照顺序发送的消息队列中之后,那么,消费者如何按照发送顺序进行消费呢?

RocketMQ的MessaqeListener回调函数提供了两种消费模式,有序消费模式MessaqeListenerOrderly和并发消费模式MessagelistenerConcurrently。所以,想要实现顺序消费,需要使用MessageListenerOrderly模式接收消息:

当我们用以上方式注册一个消费之后,为了保证同一个队列中的有序消息可以被顺序消费,就要保证RocketMO的Broker只会把消息发送到同一个消费者上,这时候就需要加锁了,

会尝试向 Broker 为当前消在实现中,ConsumeMessageOrderlyService 初始化的时候,会启动一个定时任务费者客户端申请分布式锁。如果获取成功,那么后续消息将会只发给这个Consumer。

接下来在消息拉取的过程中,消费者会一次性拉取多条消息的,并且会将拉取到的消息放入 ProcessQueue,同时将消息提交到消费线程池进行执行。

那么拉取之后的消费过程,怎么保证顺序消费呢?这里就需要更多的锁了。

RocketMQ在消费的过程中,雲要申请 MessageQueue 锁,确保在同一时间,一个队列中只有一个线程能处理队列中的消息。

获取到 MessageQueue 的锁后,就可以从ProcessQueue中依次拉取一批消息处理了,但是这个过程中,为了保证消息不会出现重复消费,还需要对ProcessQueue进行加锁。(这个在扩展知识中展开)

然后就可以开始处理业务逻辑了,

总结下来就是三次加锁,先锁定Broker上的MessageQueue,确保消息只会投递到唯一的消费者,对本地的MessageQueue加锁,确保只有一个线程能处理这个消息队列。对存储消息的ProcessQueue加锁,确保在重平衡的过程中不会出现消息的重复消费。

四、RocketMQ如何保证消息不丢失?

RocketMQ的消息想要确保不丢失,需要生产者、消费者以及Broker的共同努力,缺一不可。

首先在生产者端,消息的发送分为同步和异步两种,在同步发送消息的情况下,消息的发送会同步阻塞等待Broker返回结果,在Broker确认收到消息之后,生产者才会拿到SendResult,如果这个过程中发生异常,那么就说明消息发送可能失败了,就需要生产者进行重新发送消息。

但是Broker其实并不会立即把消息存储到磁盘上,而是先存储到内存中,内存存储成功之后,就返回给确认结果给生产者了。然后再通过异步刷盘的方式将内存中的数据存储到磁盘上。但是这个过程中,如果机器挂了,那么就可能会导致数据丢失。

如果想要保证消息不丢失,可以将消息保存机制修改为同步刷盘,这样,Broker会在同步请求中把数据保存在磁盘上,确保保存成功后再返回确认结果给生产者。

默认情况为 ASYNC_FLUSH
flushDiskType = SYNC_FLUSH

除了同步发送消息,还有异步发送,异步发送的话就需要生产者重写SendCallback的onSuccess和onException方法,用于给Broker进行回调。在方法中实现消息的确认或者重新发送。

为了保证消息不丢失,RocketMQ肯定要通过集群方式进行部署,Broker 通常采用一主多从部署方式,并且采用主从同步的方式做数据复制。

当主Broker宕机时,从Broker会接管主Broker的工作,保证消息不丢失。同时,RocketMO的Broker还可以配置多个实例,消息会在多个Broker之间进行冗余备份,从而保证数据的可性。

默认方式下,Broker在接收消息后,写入 master 成功,就可以返回确认响应给生产者了,接着消息将会异步复制到 slave 节点。但是如果这个过程中,Master的磁盘损坏了。那就会导致数据丢失了。

如果想要解决这个问题,可以配置同步复制的方式,即Master在将数据同步到Slave节点后,再返回给生产者确认结果。

## 默认为 ASYNC_MASTER
brokerRole=SYNC_MASTER

在消费者端,需要确保在消息拉取并消费成功之后再给Broker返回ACK,就可以保证消息不丢失了,如果这个过程中Broker一直没收到ACK,那么就可以重试

所以,在消费者的代码中,一定要在业务逻辑的最后一步 return consumeconcurrentlystatus.CONSUME SUCCESS;当然,也可以先把数据保存在数据库中,就返回,然后自己再慢慢处理。

但是,需要注意的是RocketMQ和Kafka一样,只能最大限度的保证消息不丢失,但是没办法做到100%保证不丢失

五、RocketMQ如何实现延时消息?

RocketMQ是支持延迟消息的,延迟消息写入到Broker后,不会立刻被消费者消费,需要等待指定的时长后才可被消费处理的消息,称为延时消息。

当消息发送到Broker后,Broker会将消息根据延迟级别进行存储。RocketMO的延迟消息实现方式是:将消息先存储在内存中,然后使用Timer定时器进行消息的延迟,到达指定的时间后再存储到磁盘中,最后投递给消费者

但是,RocketMQ的延迟消息并不是支持任意时长的延迟的,它只支持(5.0之前):1s 5s 10s 30s 1m 2m 3m4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h这几个时长。

另外,RocketMQ 5.0中新增了基于时间轮实现的定时消息。

前面提到的延迟消息,并使用Timer定时器来实现延迟投递。但是,由于Timer定时器有一定的缺陷,比如在定时器中有大量任务时,会导致定时器的性能下降,从而影响消息投递。

因此,在RocketMQ 5.0中,采用了一种新的实现方式:基于时间轮的定时消息。时间轮是一种高效的定时器算法,能够处理大量的定时任务,并且能够在0(1)时间内找到下一个即将要执行的任务,因此能够提高消息的投递性能。

并且,基于时间轮的定时消息能够支持更高的消息精度,可以实现秒级、毫秒级甚至更小时间粒度的定时消息,

具体实现方式如下:

  1. RocketMO在Broker端使用一个时间轮来管理定时消息,将消息按照过期时间放置在不同的槽位中,这样可以大幅减少定时器任务的数量。
  2. 时间轮的每个槽位对应一个时间间隔,比如1秒、5秒、10秒等,每次时间轮的滴答,槽位向前移动一个时间间隔。
  3. 当Broker接收到定时消息时,根据消息的过期时间计算出需要投递的槽位,并将消息放置到对应的槽位中。
  4. 当时间轮的滴答到达消息的过期时间时,时间轮会将该槽位中的所有消息投递给消费者。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小徐很努力

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值