RocketMQ-事务消息、顺序消费、消费重复、消息丢失、消息存储

本文详细介绍了RocketMQ的事务消息和顺序消息机制。事务消息确保了账号服务扣款与积分增加的一致性,通过半消息和回查机制保证数据完整性。顺序消息通过特定的发送策略保证消息的有序消费,而面对消息重复和丢失, RocketMQ提供了如消息幂等性、自动重试和消息持久化等解决方案。此外,还讨论了消息的存储结构,包括CommitLog和ConsumeQueue的角色。
摘要由CSDN通过智能技术生成

案例:

小明购买一个100元的东西,账户扣款100元的同时需要保证在下游的积分系统给小明这个账号增加100积分。账号系统和积分系统是两个独立是系统,一个要减少100元,一个要增加100积分。

问题:

  • 账号服务扣款成功了,通知积分系统也成功了,但是积分增加的时候失败了,数据不一致了。
  • 账号服务扣款成功了,但是通知积分系统失败了,所以积分不会增加,数据不一致了。

方案:

RocketMQ针对第一个问题解决方案是:如果消费失败了,是会自动重试的,如果重试几次后还是消费失败,那么这种情况就需要人工解决了,比如放到死信队列里然后手动查原因进行处理等。

RocketMQ针对第二个问题解决方案是:如果你扣款成功了,但是往mq写消息的时候失败了,那么RocketMQ会进行回滚消息的操作,这时候我们也能回滚我们扣款的操作。

事务消息的原理

 

详细过程

Producer发送半消息(Half Message)到broker。

  • Half Message发送成功后开始执行本地事务。
  • 如果本地事务执行成功的话则返回commit,如果执行失败则返回rollback。(这个是在事务消息的回调方法里由开发者自己决定commit or rollback)

Producer发送上一步的commit还是rollback到broker,这里有两种情况:

1.如果broker收到了commit/rollback消息 :

  • 如果收到了commit,则broker认为整个事务是没问题的,执行成功的。那么会下发消息给Consumer端消费。
  • 如果收到了rollback,则broker认为本地事务执行失败了,broker将会删除Half Message,不下发给Consumer端。

2.如果broker未收到消息(如果执行本地事务突然宕机了,相当本地事务执行结果返回unknow,则和broker未收到确认消息的情况一样处理。):

  • broker会定时回查本地事务的执行结果:如果回查结果是本地事务已经执行则返回commit,若未执行,则返回rollback。
  • Producer端回查的结果发送给Broker。Broker接收到的如果是commit,则broker视为整个事务执行成功,如果是rollback,则broker视为本地事务执行失败,broker删除Half Message,不下发给consumer。如果broker未接收到回查的结果(或者查到的是unknow),则broker会定时进行重复回查,以确保查到最终的事务结果。重复回查的时间间隔和次数都可配。

流程图

 

事务消息是个监听器,有回调函数,回调函数里我们进行业务逻辑的操作,比如给账户-100元,然后发消息到积分的mq里,这时候如果账户-100成功了,且发送到mq成功了,则设置消息状态为commit,这时候broker会将这个半消息发送到真正的topic中。一开始发送他是存到半消息队列里的,并没存在真实topic的队列里。只有确认commit后才会转移。

顺序消息

RocketMQ的消息是存储到Topic的queue里面的,queue本身是FIFO(First Int First Out)先进先出队列。所以单个queue是可以保证有序性的。

但问题是1个topic有N个queue,作者这么设计的好处也很明显,天然支持集群和负载均衡的特性,将海量数据均匀分配到各个queue上,你发了10条消息到同一个topic上,这10条消息会自动分散在topic下的所有queue中,所以消费的时候不一定是先消费哪个queue,后消费哪个queue,这就导致了无序消费。

图示:

 

一个Producer发送了m1、m2、m3、m4四条消息到topic上,topic有四个队列,由于自带的负载均衡策略,四个队列上分别存储了一条消息。queue1上存储的m1,queue2上存储的m2,queue3上存储的m3,queue4上存储的m4,Consumer消费的时候是多线程消费,所以他无法保证先消费哪个队列或者哪个消息,比如发送的时候顺序是m1,m2,m3,m4,但是消费的时候由于Consumer内部是多线程消费的,所以可能先消费了queue4队列上的m4,然后才是m1,这就导致了无序。

解决方案

问题产生的关键在于多个队列都有消息,我消费的时候又不知道哪个队列的消息是最新的。那么思路就有了,发消息的时候你要想保证有序性的话,就都给我发到一个queue上,然后消费的时候因为只有那一个queue上有消息且queue是FIFO,先进先出,所以正常消费就完了。

很完美。而且RocketMQ也给我们提供了这种发消息的时候选择queue的api(MessageQueueSelector)。

RocketMQ通过轮询所有队列的方式来确定消息被发送到哪⼀个队列(负载均衡策略)。

// RocketMQ通过MessageQueueSelector中实现的算法来确定消息发送到哪⼀个队列上// RocketMQ默认提供了两种MessageQueueSelector实现:随机/Hash// 当然你可以根据业务实现⾃⼰的MessageQueueSelector来决定消息按照何种策略发送到消息队列中SendResult sendResult = producer.send(msg, new MessageQueueSelector() {

 @Override

 public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg){

 Integer id = (Integer) arg;

 int index = id % mqs.size();

 return mqs.get(index);

 }}, orderId);

在获取到路由信息以后,会根据MessageQueueSelector实现的算法来选择⼀个队列,同⼀个OrderId 获取到的肯定是同⼀个队列。(orderId就是自定义参数arg)

消息重复

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

所以解决这个问题的办法就是绕过这个问题。那么问题就变成了:如果消费端收到两条一样的消息,应该怎样处理?

  1. 消费端处理消息的业务逻辑保持幂等性
  2. 保证每条消息都有唯一编号且保证消息处理成功与去重表的日志同时出现

第1条很好理解,只要保持幂等性,不管来多少条重复消息,最后处理的结果都一样。

第2条原理就是利用一张日志表来记录已经处理成功的消息的ID,如果新到的消息ID已经在日志表中,那么就不再处理这条消息。

第1条解决方案,很明显应该在消费端实现,不属于消息系统要实现的功能。

第2条可以消息系统实现,也可以业务端实现。正常情况下出现重复消息的概率其实很小,如果由消息系统来实现的话,肯定会对消息系统的吞吐量和高可用有影响,所以最好还是由业务端自己处理消息重复的问题,这也是RocketMQ不解决消息重复的问题的原因。

怎么保证消息不丢失

消息发送过程

 

我们将消息流程分为如下三大部分,每一部分都有可能会丢失数据。

  • 生产阶段:Producer通过网络将消息发送给Broker,这个发送可能会发生丢失,比如网络延迟不可达等。
  • 存储阶段:Broker肯定是先把消息放到内存的,然后根据刷盘策略持久化到硬盘中,刚收到Producer的消息,再内存中了,但是异常宕机了,导致消息丢失。
  • 消费阶段:消费失败了其实也是消息丢失的一种变体吧

Producer生产阶段

1.解决方案一

有三种send方法,同步发送、异步发送、单向发送。我们可以采取同步发送的方式进行发送消息,发消息的时候会同步阻塞等待broker返回的结果,如果没成功,则不会收到SendResult,这种是最可靠的。其次是异步发送,再回调方法里可以得知是否发送成功。单向发送(OneWay)是最不靠谱的一种发送方式,我们无法保证消息真正可达。

/** * {@link org.apache.rocketmq.client.producer.DefaultMQProducer} */

// 同步发送public SendResult send(Message msg) throws MQClientException, RemotingException,      MQBrokerException, InterruptedException {}

// 异步发送,sendCallback作为回调public void send(Message msg,SendCallback sendCallback) throws MQClientException, RemotingException, InterruptedException {}

// 单向发送,不关心发送结果,最不靠谱public void sendOneway(Message msg) throws MQClientException, RemotingException, InterruptedException {}

2.解决方案二

发送消息如果失败或者超时了,则会自动重试。默认是重试三次,可以根据api进行更改,比如改为10次:

producer.setRetryTimesWhenSendFailed(10);

Broker存储阶段

1.解决方案一

MQ持久化消息分为两种:同步刷盘和异步刷盘。默认情况是异步刷盘,Broker收到消息后会先存到cache里然后立马通知Producer说消息我收到且存储成功了,你可以继续你的业务逻辑了,然后Broker起个线程异步的去持久化到磁盘中,但是Broker还没持久化到磁盘就宕机的话,消息就丢失了。同步刷盘的话是收到消息存到cache后并不会通知Producer说消息已经ok了,而是会等到持久化到磁盘中后才会通知Producer说消息完事了。这也保障了消息不会丢失,但是性能不如异步高。看业务场景取舍。

## 默认情况为 ASYNC_FLUSH修改为同步刷盘SYNC_FLUSH实际场景看业务同步刷盘效率肯定不如异步刷盘高flushDiskType = SYNC_FLUSH 

2.解决方案二

集群部署,主从模式,高可用。

即使Broker设置了同步刷盘策略,但是Broker刷完盘后磁盘坏了,这会导致盘上的消息全TM丢了。但是如果即使是1主1从了,但是Master刷完盘后还没来得及同步给Slave就磁盘坏了,不也是GG吗?没错!

所以我们还可以配置不仅是等Master刷完盘就通知Producer,而是等Master和Slave都刷完盘后才去通知Producer说消息ok了。

若想很严格的保证Broker存储消息阶段消息不丢失,则需要如下配置,但是性能肯定远差于默认配置。

# master 节点配置flushDiskType = SYNC_FLUSHbrokerRole=SYNC_MASTER

# slave 节点配置brokerRole=slaveflushDiskType = SYNC_FLUSH

Producer发消息到Broker后,Broker的Master节点先持久化到磁盘中,然后同步数据给Slave节点,Slave节点同步完且落盘完成后才会返回给Producer说消息ok了。

Consumer消费阶段

1.解决方案一

消费者会先把消息拉取到本地,然后进行业务逻辑,业务逻辑完成后手动进行ack确认,这时候才会真正的代表消费完成。而不是说pull到本地后消息就算消费完了。举个例子

consumer.registerMessageListener(new MessageListenerConcurrently() {

     @Override

     public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {

         for (MessageExt msg : msgs) {

             String str = new String(msg.getBody());

             System.out.println(str);

         }

         // ack,只有等上面一系列逻辑都处理完后,到这步CONSUME_SUCCESS才会通知broker说消息消费完成,         //如果上面发生异常没有走到这步ack,则消息还是未消费状态。而不是像比如redis的blpop,         //弹出一个数据后数据就从redis里消失了,并没有等我们业务逻辑执行完才弹出。         return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;

     }

 });

2.解决方案二

消息消费失败自动重试。如果消费消息失败了,没有进行ack确认,则会自动重试,重试策略和次数(默认15次)

消息被消费后会立即删除吗?

不会,每条消息都会持久化到CommitLog中,每个Consumer连接到Broker后会维持消费进度信息,当有消息消费后只是当前Consumer的消费进度(CommitLog的offset)更新了。

默认48小时后会删除不再使用的CommitLog文件

消息存储

RocketMQ中最核心的一个环节就是Broker中的消息数据存储,也就是所谓的消息持久化。

RocketMQ 在持久化的设计上,采取的是「消息顺序写、随机读的策略」,利用磁盘顺序写的速度,让磁盘的写速度不会成为系统的瓶颈。并且采用 MMPP 这种“零拷贝”技术,提高消息存盘和网络发送的速度。极力满足 RocketMQ 的高性能、高可靠要求。

 

RocketMQ的消息存储是由ComsumeQueue 和 CimmitLog配合完成的

在RocketMQ中,所有topic的消息都存储在⼀个称为CommitLog的文件中,该文件默认最大为1GB,超过1GB后会轮到下⼀个CommitLog文件。通过CommitLog,RocketMQ将所有消息存储在⼀起,以顺序IO的方式写⼊磁盘,充分利用了磁盘顺序写减少了IO争用提高数据存储的性能。

⼀个ConsumeQueue表示⼀个topic的⼀个queue,RocketMQ的ConsumeQueue中不存储具体的消息,具体的消息由CommitLog存储,ConsumeQueue中只存储路由到该queue中的消息在CommitLog中的offset。

看下存储时的逻辑图

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值