RocketMQ

RocketMQ是阿里团队用Java语言开发的一款开源组件,现已被Apache列为顶尖项目。它借鉴Kafka的设计思想,但又与Kafka不同。相比于Kafka,RocketMQ做了架构上的简化,功能上的优化。性能没有Kafka好,但功能丰富。

RocketMQ的运行架构图

RocketMQ的消息模型

消息确认机制

RocketMQ要支持互联网金融场景,那么消息安全是必须优先保障的。而消息安全有两方面的要求,一方面是生产者要能确保将消息发送到Broker上,另一方面是消费者要能确保从Broker上获取到消息。

消息生产端采用消息确认加多次重试的机制保证消息正常发送到Broker

针对消息发送的不确定性,封装了三种发送消息的方式:

  1. 单向发送
    单向发送方式下,消息生产者只管往Broker发送消息,而全然不关心Broker端有没有成功接收到消息。
    public class OnewayProducer {
        public static void main(String[] args) throws Exception {
            DefaultMQProducer producer = new DefaultMQProducer("producerGroup");
            producer.start();
            Message message = new Message("Order", "tag", "order info : orderId = xxx".getBytes(StandardCharsets.UTF_8));
            producer.sendOneway(message);
            Thread.sleep(50000);
            producer.shutdown();
        }
    }

    sendOneway方法没有返回值,如果发送失败,生产者无法补救。
    单向发送有一个好处,就是发送消息的效率更高。适用于一些追求消息发送效率,而允许消息丢失的业务场景,比如日志。

  2. 同步发送
    同步发送方式下,消息生产者在往Broker端发送消息后,会阻塞当前线程,等待Broker端的相应结果。
     

    //发送消息,获取发送结果
    SendResult sendResult = producer.send(message, 20000);

    SendResult来自于Broker的反馈。producer在send发出消息,到Broker返回SendResult的过程中,无法做其他的事情。
    在SendResult中有一个SendStatus属性,这个SendStatus是一个枚举类型,其中包含Broker端的各种情况。

    public enum SendStatus {
        SEND_OK,
        FLUSH_DISK_TIMEOUT,
        FLUSH_SLAVE_TIMEOUT,
        SLAVE_NOT_AVAILABLE;
    }

            在这几种枚举值中,SEND_OK表示消息已经成功发送到Broker上。至于其他几种枚举值,都是表示消息在Broker端处理失败了。使用同步发送的机制,我们就可以在消息生产者发送完消息后,对发送失败的消息进行补救。例如重新发送。
            但是此时要注意,如果Broker端返回的SendStatus不是SEND_OK,也并不表示消息就一定不会推送给下游的消费者。仅仅只是表示Broker端并没有完全正确的处理这些消息。因此如果要重新发送消息,最好要带上唯一的系统标识,这样在消费端,才能自行做幂等判断。也就是用具有业务含义的OrderID这样的字段来判断消息有没有被重复处理。
             这种同步发送的机制能够很大程度上保证消息发送的安全性。但是,这种同步发送机制的发送效率比较低。毕竟,send方法需要消息在生产者和Broker之间传输一个来回后才能结束。如果网速比较慢,同步发送的耗时就会很长。

  3. 异步发送
    异步发送机制下,生产者在向Broker发送消息时,会同时注册一个回调函数。接下来生产者并不等待Broker的响应。当Broker端有响应数据过来时,自动触发回调函数进行对应的处理。

消息消费者端采用状态确认机制保证消费者一定能正常处理对应的消息

Broker等待消费者返回消息处理状态。

// 注册一个回调函数,消费到消息后就会触发回滚
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                msgs.forEach(messageExt -> {
                    try {
                        System.out.println("收到消息:" + new String(messageExt.getBody(), RemotingHelper.DEFAULT_CHARSET));
                    } catch (UnsupportedEncodingException e) {
                        e.printStackTrace();
                    }
                });
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

返回值是一个枚举值,有两个选项,CONSUME_SUCCESS和RECONSUME_LATER。如果消费者返回CONSUME_SUCCESS,那么消息自然就处理结束了。但是如果消息没有处理成功,返回的是RECONSUME_LATER,Broker就会过一段时间再发起消息重试。

记录一次使用RocketMQ中遇到的异常

若使用阿里云的服务器,RocketMQ客户端通过公网IP连接NameServer后,却获取到Broker的私网IP导致连接失败。这是典型的Broker向NameServer注册私网IP,而客户端在公网环境无法访问该私网IP的问题。RocketMQ的Broker启动时,会自动向NameServer注册自己的IP地址(默认取本机内网IP)。在阿里云服务器中,机器通常有私网IP和公网IP。

  • 若Broker未特殊配置,会默认将私网IP注册到NameServer
  • 客户端通过公网连接NameServer后,NameServer返回的是Broker的私网IP,而客户端在公网环境无法访问该私网IP,最终导致RemotingConnectException

解决办法:

  1. 修改Broker配置文件,指定公网IP

    Broker的配置文件通常为rocketmq/conf/broker.conf(若未自定义,可复制conf/broker.conf作为模板),添加以下配置:
     

    #关键配置:指定Broker向NameServer注册的IP(公网IP)
    
    brokerIP1=xx.xxx.xx.xx       #替换为Broker所在服务器的公网IP
  2. 重启Broker,使配置生效
    停止当前运行Broker,使用修改后的配置文件重新启动(需指定配置文件路径)

    #停止Broker(若已启动)
    sh bin/mqshutdown broker
    #启动Broker,指定配置文件和NameServer地址
    nohup sh bin/mqbroker -n xx.xx..xx.xx:9876 -c conf/broker.conf & #替换为实际的NameServer地址(公网或私网)
  3. 检查Broker注册的IP是否正确
    通过NameServer的命令行工具,验证Broker注册的IP是否为指定的公网IP:

    #查看NameServer中的Broker信息(在NameServer所在服务器执行)
    sh bin/mqadmin clusterList -n 127.0.0.1:9876    #本地NameServer用127.0.0.1,远程用公网IP

搭建RocketMQ可视化管理服务

RocketMQ提供了一个图形化的管理控制台Dashboard,可以用可视化的方式直接观测并管理RocketMQ的运行过程

需要到RocketMQ的官网下载Dashboard服务,最终获得Dashboard的jar包,获得jar包后,可按以下步骤操作,启动服务。

  1. 在jar包所在目录下创建application.yml文件,在配置文件中添加以下配置:
    rocketmq:
      config:
        namesrvAddrs:
          - 8.141.10.20:9876 #指定nameserver地址
    关于这个配置文件中的更多配置选项,可以参考一下dashboard源码中的application.yml文件
  2. 通过一下命令,启动管理控制台服务
     
    java -jar rocketmq-dashboard-1.0.1-SNAPSHOT.jar 1>dashboard.log 2>&1 &
  3. 服务启动完成后,会在服务器端搭建起一个web服务,可以通过http://ip:8080访问,默认8080端口,可自行修改

顺序消费机制

要保证消息顺序消费,需要从生产端、消息队列、消费端三方面来考虑。保证顺序性的同时,需要牺牲一定的性能。

  1. 生产者:
    若需要保证严格顺序性,则需要使用单一生产者节点,比如股票出价场景。
    若使用生产者集群,则需要利用分布式锁,保证串行发送,如秒杀场景,没有严苛的顺序性。
    保证需按序处理的消息(如同一订单的消息路由到同一个Queue)。实现这一目标依赖MessageQueueSelector(队列选择器)。

    // 生产端顺序消费消息
            for (int i = 0; i < 10; i++) {
                int orderId = i;
                for (int j = 0; j <= 5; j++) {
                    Message message = new Message("OrderTopicTest", "order_" + orderId, "KEY" + orderId, ("order_" + orderId + "step" + j).getBytes(RemotingHelper.DEFAULT_CHARSET));
                    // 通过MessageQueueSelector保证消息在同一组。将orderId相同的消息,都转发到同一个MessageQueue中
                    SendResult sendResult = producer.send(message, new MessageQueueSelector() {
                        @Override
                        public MessageQueue select(List<MessageQueue> mqs, Message message, Object arg) {
                            // 根据userId选择队列
                            Integer id = (Integer) arg;
                            int index = id % mqs.size();
                            return mqs.get(index);
                        }
                    }, orderId);
                }
            }
  2. 消息队列:基于Queue的FIFO保序
    存储端无需额外操作,仅依赖RocketMQ的原生存储逻辑即可保证顺序。局部有序,一组业务需要保证单一队列。
    消息写入顺序:生产者发送的消息会直接写入选中的Queue,且Queue内部按“消息偏移量(Offset)”递增顺序存储(Offset越小,消息发送时间越早)
    持久化顺序:RocketMQ的存储层(CommitLog+ConsumeQueue)会确保Queue内的消息持久化顺序与写入顺序一致——ConsumeQueue作为消息的“索引文件”,会按Offset顺序记录消息在CoomitLog中的位置,消费端通过ConsumeQueue读取消息时,自然遵循Offset递增顺序。

    1. 消费端:保证同一Queue消息串行处理
      消费端的核心任务是:对同一Queue的消息进行串行处理

      保证一组业务让消费者组中的固定消费者去消费,并不能使用多线程消费。代码中调用相应的顺序消费方法。
      关键设计
      消费端负载均衡机制:RocketMQ的集群模式下,Broker会将Topic的Queue均匀分配给消费组内的消费者实例(避免多实例并行消费同一Queue
      专用顺序消费监听器:消费端需使用MessageListenerOrderly(顺序消费监听器),而非并发消费的MessageListenerConcurrently。MessageListenerOrderly会为每个Queue绑定一个独立的消费线程(或通过锁保证串行),确保同一Queue的消息“处理完前一条,再处理后一条”。
      MessageListenerOrderly内部已通过锁保证同一Queue串行,若消费端在监听器中手动开启多线程处理(如提交到线程池),会直接破坏顺序。
      禁止乱序重试:若某条消息消费失败(如抛出异常),RocketMQ会对消息进行重试,但重试期间会阻塞该Queue的后续消息(直到重试成功或进入死信队列),避免重试消息晚于后续消息被处理。
       consumer.registerMessageListener(new MessageListenerOrderly() {
                  @Override
                  public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext contxt) {
                      contxt.setAutoCommit(true);
                      for (MessageExt msg : msgs) {
                          System.out.println("收到消息内容" + new String(msg.getBody()));
                      }
                      return ConsumeOrderlyStatus.SUCCESS;
                  }
              });
  1. 生产者只有将一批有顺序要求的消息,放到同一个MessageQueue上,通过MessageQueue的FIFO特性保证这一消息的顺序性
    如果不指定MessageSelector对象,那么生产者会采用轮询的方式将多条消息依次发送到不同的MessaeQueue上
  2. 消费者需要实现MessageListenerOrderly接口,实际上在服务端,处理MessageListenerOrderly时,会给一个MessageQueue加锁,拿到MessageQueue上的所有消息,然后再去犯下一个MessageQueue的消息

事务消息机制

RocketMQ的事务消息机制是分布式事务的一种降级实现方案。它确保本地事务操作与消息发送能保持最终一致性,避免出现“本地事务成功但消息未发送”或“消息发送成功但本地事务失败”的不一致情况。

使用业务场景举例

典型的下单场景,添加订单信息、添加物流信息、积分服务、清空购物车

具体实现思路:

  1. 生产者将消息发送至Apache RocketMQ服务端(broker)
  2. Apache RocketMQ服务端将消息持久化之后,向生产者返回Ack确认消息以及发送成功,此时消息被标记为“暂不能投递”,在这种状态下的消息即为半事务消息,其实是将消息转存到了一系统主题,并持久化到磁盘。
  3. 生产者执行本地事务
  4. 生产者根据本地事务执行结果向服务器提交二次确认结果(Commit或是Rollback),服务端收到确认结果后处理逻辑如下:
    二次确认结果为Commit:服务端将半事务消息标记为可投递,并投递给消费者
    二次确认结果为Rollback:服务端将回滚事务,不会将半事务消息投递给消费者
  5. 在断网或是生产者应用重启的特殊情况下,若服务端未收到发送者提交的二次确认结果,或服务端收到的二次确认结果为Unknown未知状态,经过固定时间后,服务端将对生产者集群中任一生产者实例发起消息回查。
  6. 生产者收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
  7. 生产者根据回查到的本地事务的最终状态再次向服务端提交二次确认,服务端将按照步骤4对半事务消息进行处理。

实现时的重点是使用RocketMQ提供的TransactionMQProducer事务生产者,在TransactionMQProducer中注入一个TransactionListener事务监听器来执行本地事务,以及后续对本地事务的检查。

示例代码:

public class TransactionProducer {
    public static final String PRODUCER_GROUP = "TransactionProducer";
    public static final String DEFAULT_NAMESRVADDR = "8.141.10.20:9876";
    public static final String TOPIC = "TransTopic";
    public static final int MESSAGE_COUNT = 10;

    public static void main(String[] args) throws Exception {
        TransactionListener transactionListener = new TransactionListenerImpl();
        TransactionMQProducer producer = new TransactionMQProducer(PRODUCER_GROUP, Arrays.asList(TOPIC));
        producer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
        ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2000), r -> {
            Thread thread = new Thread(r);
            thread.setName("client-transaction-msg-check-thread");
            return thread;
        });
        producer.setExecutorService(executorService);
        producer.setTransactionListener(transactionListener);
        producer.start();

        String[] tags = new String[]{"TagA", "TagB", "TagC", "TagD", "TagE"};
        for (int i = 0; i < MESSAGE_COUNT; i++) {
            try {
                Message message = new Message(TOPIC, tags[i % tags.length], "KEY" + i, ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
                SendResult sendResult = producer.sendMessageInTransaction(message, null);
                System.out.printf("%s%n", sendResult);
                Thread.sleep(10);
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            } catch (MQClientException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        for (int i = 0; i < 100000; i++) {
            Thread.sleep(1000);
        }
        producer.shutdown();
    }

事务监听器:

public class TransactionListenerImpl implements TransactionListener {

    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        String tags = msg.getTags();
        System.out.println("executeLocalTransaction tags:" + tags);
        switch (tags) {
            case "TagA":
                return LocalTransactionState.COMMIT_MESSAGE;
            case "TagB":
                return LocalTransactionState.ROLLBACK_MESSAGE;
        }
        return LocalTransactionState.UNKNOW;
    }

    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
        String tags = messageExt.getTags();
        System.out.println("checkLocalTransaction tags:" + tags);
        switch (tags) {
            case "TagC":
                return LocalTransactionState.COMMIT_MESSAGE;
            case "TagD":
                return LocalTransactionState.ROLLBACK_MESSAGE;
        }
        return LocalTransactionState.UNKNOW;
    }
}

RocketMQ如何保证本地事务执行成功,消息被正确发送并被消费者处理

RocketMQ确保“本地事务执行成功则消息必定被正确发送并被消费者处理”的核心逻辑,是通过半事务消息的可靠存储、事务状态的明确确认、异常情况下的回查补偿以及消息投递的重试机制共同实现的,具体可拆解为以下关键环节:

  1. 半事务消息的“可靠暂存”:确保消息不会丢失
    发送方首先发送“半事务消息”到Broker时,Broker会执行以下操作:
    将消息写入本地存储(CommitLog),并标记为“暂存状态”(对消费者不可见)
    向发送方返回“消息发送成功”的确认(类似“预提交成功”)
    这一步的关键是:半消息一旦被Broker确认接收,就会被持久化到磁盘(通过刷盘机制确保,可配置同步刷盘或异步刷盘,同步刷盘保证消息不丢失)。即使Broker宕机重启,也能从磁盘恢复该消息,为后续处理奠定基础。

  2. 本地事务成功后的“明确提交”:主动触发消息可见
    当发送方的本地事务执行成功后,会通过以下步骤确保消息被处理:
    发送方向Broker发送COMMIT_MESSAGE指令,明确告知“本地事务已成功,消息可投递”。
    Broker收到COMMIT指令后,会将半事务消息的状态从“暂存”改为“可见”,并加入消费者的消息队列,使其可被消费者拉取。
    这一步通过“主动提交机制”,确保本地事务成功与消息可见性的直接关联。

  3. 异常场景的“事务回查”:补偿未收到的提交指令
    若发送方在本地事务成功后,因网络中断、服务崩溃等原因,未能向Broker发送COMMIT指令,Broker会通过“事务回查”机制进行补偿:
    触发条件:Broker会对“长时间未收到明确状态”(UNKNOWN状态)的半事务消息,启动定时回查(默认每隔一段时间,最多回查15次,可配置)
    回查逻辑:Broker调用发送方预先注册的TransactionListener.checkLocalTransaction(),查询本地事务的实际执行结果
    处理结果:若回查发现“本地事务已成功”,Broker会自动将消息标记为COMMIT状态,使其对消费者可见。
    这一步解决了“本地事务成功但指令丢失”的异常场景,通过主动探活确保状态一致。

  4. 消息投递的“重试机制”:确保消费者最终处理
    即使消息已被标记为“可见”,仍可能因消费者故障、网络问题导致处理失败。RocketMQ通过以下机制保证消费者最终处理:
    消费者确认机制:消费者需显示返回CONSUME_SUCCESS确认消息已处理,否则Broker会认为消息处理失败。
    重试队列:处理失败的消息会被放入“重试队列”,Broker会按照指数退避策略(间隔逐渐增大)重新投递,直到消费者成功处理或达到最大重试次数(默认16次,可配置)。
    死信队列:若达到最大重试次数仍失败,消息会进入“死信队列”,供人工干预处理,避免消息丢失

  5. 底层存储的“可靠性保障”:防止Broker数据丢失
    Broker对消息的持久化设计进一步保障类可靠性:
    多副本机制:若配置了Broker主从架构,消息会同步到从节点,主节点宕机后从节点可接管服务,避免消息丢失。
    刷盘策略:通过同步刷盘(消息写入即刷盘)或异步刷盘(批量刷盘)确保消息落地,同步刷盘可保证极端情况下(如Broker断电)消息不丢失。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值